Compare commits

..

19 Commits

Author SHA1 Message Date
Matt Aitchison
2163cff4d1 feat(flow): log each flow method execution at INFO
Flows emit a Rich console message when they start ('Flow started with
ID: …') but produce no further log line until the next significant
event. When a flow appears to hang, this makes it hard to tell which
@start/@listen method is currently running.

Add a single INFO log at the entry of _execute_method that surfaces
the active method name. This works for both @start methods and
listeners since both paths funnel through _execute_method.
2026-05-19 14:45:58 -05:00
Greyson LaLonde
35f693cf68 chore: tighten typing across plus_api client
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Vulnerability Scan / pip-audit (push) Waiting to run
Adds typed containers for wire payloads, literal aliases for HTTP method
and log type, and Ffnal markers on resource constants. Updates
upstream returns in project_utils.py and deploy/main.py to match
the new contracts.
2026-05-20 01:43:48 +08:00
Greyson LaLonde
da15554d81 feat: generate categorized release notes for enterprise 2026-05-20 00:24:26 +08:00
Greyson LaLonde
284533464f fix: bump idna to 3.15 to address GHSA-65pc-fj4g-8rjx
Some checks failed
Build uv cache / build-cache (3.10) (push) Waiting to run
Build uv cache / build-cache (3.11) (push) Waiting to run
Build uv cache / build-cache (3.12) (push) Waiting to run
Build uv cache / build-cache (3.13) (push) Waiting to run
Check Documentation Broken Links / Check broken links (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
2026-05-19 23:38:34 +08:00
Tiago Freire
024e230b2c docs: remove {" "} JSX expressions breaking <Steps> render (#5857)
## Overview

Prettier-inserted bare `{" "}` lines between sibling `<Step>` elements caused Mintlify's `<Steps>` to crash with "Cannot read properties of undefined (reading 'stepNumber')", leaving the page body blank.

### Affected pages (en/ar/ko/pt-BR):
- enterprise/guides/enable-crew-studio
- learn/llm-selection-guide
2026-05-19 10:44:53 -04:00
Greyson LaLonde
a4c90b6912 docs: update changelog and version for v1.14.5
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2026-05-19 03:19:40 +08:00
Greyson LaLonde
c50da7a6f2 feat: bump versions to 1.14.5 2026-05-19 03:11:26 +08:00
Irfaan Mansoori
e8aa870f90 fix: memory leak in git.py by using cached_property
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
2026-05-18 21:55:57 +08:00
Greyson LaLonde
14cd81eec6 docs: update changelog and version for v1.14.5a7 2026-05-18 21:13:34 +08:00
Greyson LaLonde
a6225da326 feat: bump versions to 1.14.5a7 2026-05-18 21:08:46 +08:00
Greyson LaLonde
259d334e38 chore(devtools): skip pinning crewai-files in file-processing extra 2026-05-18 21:00:37 +08:00
Greyson LaLonde
42aa8a777c chore: deprecate function_calling_llm field 2026-05-18 20:49:11 +08:00
Heitor Carvalho
a95d26763f docs: update changelog and version for v1.14.5a6 (#5828)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2026-05-15 17:05:04 -03:00
Heitor Carvalho
65ec783aae feat: bump versions to 1.14.5a6 (#5827) 2026-05-15 16:51:59 -03:00
Greyson LaLonde
eefe0e42ac fix: surface streamed tool calls when available_functions is absent 2026-05-16 02:46:35 +08:00
Greyson LaLonde
75bb882911 fix(deps): bump langsmith to >=0.8.0 for GHSA-3644-q5cj-c5c7
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
2026-05-15 21:32:52 +08:00
iris-clawd
c36827b45b fix(docs/pt-BR): replace untranslated code block placeholders (#5781)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
* fix(docs/pt-BR): replace untranslated code block placeholders

Replace all `# (O código não é traduzido)` and `# código não traduzido`
placeholder comments in the PT-BR docs with the actual code from the
English source files.

Files fixed:
- docs/pt-BR/concepts/flows.mdx (~15 placeholders → real code)
- docs/pt-BR/guides/flows/mastering-flow-state.mdx (~17 placeholders → real code)

Code itself is kept in English per i18n conventions. Inline # comments
within code blocks have been translated to Portuguese.

* fix(docs/pt-BR): address CodeRabbit review comments

- flows.mdx: add missing load_dotenv() call after imports
- mastering-flow-state.mdx: fix PersistentCounterFlow second-run example
  to pass inputs={"id": flow1.state.id} to kickoff(), matching the
  documented resume pattern; update comment accordingly
2026-05-13 12:23:18 -03:00
Lorenze Jay
264da8245a Lorenze/imp/prompt layering (#5774)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
* improving prompt structure especially for prompt caching

* addressing comments
2026-05-12 12:39:12 -07:00
Mani
f2960ccaaf Added docs for TavilyGetResearch (#5707)
* Add Tavily Research and get Research

- Added tavily research with docs to crew AI

- Added tavily get research with docs to crew AI

* Update `tavily-python` installation instructions and adjust version constraints

- Changed installation command from `pip install` to `uv add` for `tavily-python` in multiple documentation files.
- Updated version constraint for `tavily-python` in `pyproject.toml` from `>=0.7.14` to `~=0.7.14`.
- Modified the `exclude-newer` date in `uv.lock` to `2026-04-23T07:00:00Z`.

* Add Tavily Research Tool documentation in multiple languages

- Introduced `TavilyResearchTool` documentation in English, Arabic, Korean, and Portuguese.
- Updated `docs.json` to include paths for the new documentation files.
- The `TavilyResearchTool` allows CrewAI agents to perform multi-step research tasks and generate cited reports using the Tavily Research API.

* Fix Tavily research CI failures

* added getResearchTool docs

- Added docs for getResearchTool

---------

Co-authored-by: lorenzejay <lorenzejaytech@gmail.com>
Co-authored-by: Evan Rimer <evan.rimer@tavily.com>
Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com>
2026-05-12 12:25:45 -07:00
57 changed files with 4129 additions and 262 deletions

View File

@@ -4,6 +4,86 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="19 مايو 2026">
## v1.14.5
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5)
## ما الذي تغير
### الميزات
- إلغاء استخدام `CrewAgentExecutor`، وتعيين وكلاء الطاقم الافتراضيين إلى `AgentExecutor`
- تحسين أدوات صندوق الرمل Daytona
- إضافة معلمة بدء `restore_from_state_id`
- إضافة تسليط الضوء على `ExaSearchTool`، وإعادة تسميته من `EXASearchTool`
### إصلاحات الأخطاء
- إصلاح تسرب الذاكرة في `git.py` باستخدام `cached_property`
- عرض استدعاءات الأدوات المتدفقة عندما تكون `available_functions` غائبة
- ضمان تحميل أحداث `skills` للتتبع
- تصحيح مسار نقطة النهاية للحالة من `/{kickoff_id}/status` إلى `/status/{kickoff_id}`
- استعادة كتلة الشيفرة المفقودة في دليل التدفق الأول للغة البرتغالية (pt-BR)
- منع `result_as_answer` من إرجاع رسائل الخطأ أو الكتل المرتبطة كإجابة نهائية
- الحفاظ على مخرجات المهام عبر تفريغ الدفعات غير المتزامنة
- دائمًا استعادة `task.output_pydantic` في كتلة finally
- التعامل مع إدخال `BaseModel` في `convert_to_model`
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.5
- إضافة دليل ترقية OSS و انتقال الطاقم إلى التدفق
- توثيق متغيرات البيئة الإضافية لأدوات المطور
- إضافة وثائق لـ `TavilyGetResearch`
### إعادة الهيكلة
- استخراج واجهة سطر الأوامر إلى حزمة مستقلة `crewai-cli`
## المساهمون
@NIK-TIGER-BILL, @akaKuruma, @cgoeppinger, @github-actions[bot], @greysonlalonde, @heitorado, @irfaan101, @iris-clawd, @lorenzejay, @manisrinivasan2k1, @minasami-pr, @mislavivanda, @theCyberTech, @theishangoswami, @wishhyt
</Update>
<Update label="18 مايو 2026">
## v1.14.5a7
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a7)
## ما الذي تغير
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.5a6
### تغييرات كسرية
- إلغاء حقل function_calling_llm
## المساهمون
@greysonlalonde, @heitorado
</Update>
<Update label="15 مايو 2026">
## v1.14.5a6
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a6)
## ما الذي تغير
### إصلاحات الأخطاء
- إصلاح استدعاءات الأدوات المتدفقة عندما تكون available_functions غائبة
- رفع اعتماد langsmith إلى الإصدار >=0.8.0 لمعالجة GHSA-3644-q5cj-c5c7
- حل مشاكل الأماكن الشاغرة لكتل التعليمات البرمجية غير المترجمة في وثائق البرتغالية البرازيلية
### الوثائق
- إضافة وثائق لـ TavilyGetResearch
- تحديث سجل التغييرات والإصدار لـ v1.14.5a5
## المساهمون
@greysonlalonde, @heitorado, @iris-clawd, @lorenzejay, @manisrinivasan2k1
</Update>
<Update label="13 مايو 2026">
## v1.14.5a5

View File

@@ -29,6 +29,7 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

View File

@@ -146,7 +146,6 @@ Crew Studio هو طريقة مبتكرة لإنشاء طواقم وكلاء ال
</Step>
{" "}
<Step title="الإجابة على الأسئلة">
أجب على أسئلة التوضيح من مساعد الطاقم لتنقيح
متطلباتك.
@@ -161,12 +160,10 @@ Crew Studio هو طريقة مبتكرة لإنشاء طواقم وكلاء ال
</Step>
{" "}
<Step title="الموافقة أو التعديل">
وافق على الخطة أو اطلب تغييرات إذا لزم الأمر.
</Step>
{" "}
<Step title="التنزيل أو النشر">
نزّل الكود للتخصيص أو انشر مباشرة على المنصة.
</Step>

View File

@@ -802,7 +802,6 @@ The tables below show a representative sample of current top-performing models a
Begin with well-established models like **GPT-4.1**, **Claude 3.7 Sonnet**, or **Gemini 2.0 Flash** that offer good performance across multiple dimensions and have extensive real-world validation.
</Step>
{" "}
<Step title="Identify Specialized Needs">
Determine if your crew has specific requirements (coding, reasoning, speed)
that would benefit from specialized models like **Claude 4 Sonnet** for
@@ -810,7 +809,6 @@ The tables below show a representative sample of current top-performing models a
consider fast inference providers like **Groq** alongside model selection.
</Step>
{" "}
<Step title="Implement Multi-Model Strategy">
Use different models for different agents based on their roles.
High-capability models for managers and complex tasks, efficient models for

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,86 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="May 19, 2026">
## v1.14.5
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5)
## What's Changed
### Features
- Deprecate `CrewAgentExecutor`, default Crew agents to `AgentExecutor`
- Improve Daytona sandbox tools
- Add `restore_from_state_id` kickoff parameter
- Add highlights to `ExaSearchTool`, rename from `EXASearchTool`
### Bug Fixes
- Fix memory leak in `git.py` by using `cached_property`
- Surface streamed tool calls when `available_functions` is absent
- Ensure `skills` loading events for traces
- Correct status endpoint path from `/{kickoff_id}/status` to `/status/{kickoff_id}`
- Restore missing code block in pt-BR first-flow guide
- Prevent `result_as_answer` from returning hook-block or error messages as final answer
- Preserve task outputs across async batch flush
- Always restore `task.output_pydantic` in finally block
- Handle `BaseModel` input in `convert_to_model`
### Documentation
- Update changelog and version for v1.14.5
- Add OSS upgrade & crew-to-flow migration guide
- Document additional env vars for devtools
- Add docs for `TavilyGetResearch`
### Refactoring
- Extract CLI into standalone `crewai-cli` package
## Contributors
@NIK-TIGER-BILL, @akaKuruma, @cgoeppinger, @github-actions[bot], @greysonlalonde, @heitorado, @irfaan101, @iris-clawd, @lorenzejay, @manisrinivasan2k1, @minasami-pr, @mislavivanda, @theCyberTech, @theishangoswami, @wishhyt
</Update>
<Update label="May 18, 2026">
## v1.14.5a7
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a7)
## What's Changed
### Documentation
- Update changelog and version for v1.14.5a6
### Breaking Changes
- Deprecate function_calling_llm field
## Contributors
@greysonlalonde, @heitorado
</Update>
<Update label="May 15, 2026">
## v1.14.5a6
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a6)
## What's Changed
### Bug Fixes
- Fix streamed tool calls when available_functions is absent
- Bump langsmith dependency to version >=0.8.0 to address GHSA-3644-q5cj-c5c7
- Resolve untranslated code block placeholders in Brazilian Portuguese documentation
### Documentation
- Add documentation for TavilyGetResearch
- Update changelog and version for v1.14.5a5
## Contributors
@greysonlalonde, @heitorado, @iris-clawd, @lorenzejay, @manisrinivasan2k1
</Update>
<Update label="May 13, 2026">
## v1.14.5a5

View File

@@ -29,6 +29,7 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

View File

@@ -146,7 +146,6 @@ Here's a typical workflow for creating a crew with Crew Studio:
</Step>
{" "}
<Step title="Answer Questions">
Respond to clarifying questions from the Crew Assistant to refine your
requirements.
@@ -161,12 +160,10 @@ Here's a typical workflow for creating a crew with Crew Studio:
</Step>
{" "}
<Step title="Approve or Modify">
Approve the plan or request changes if necessary.
</Step>
{" "}
<Step title="Download or Deploy">
Download the code for customization or deploy directly to the platform.
</Step>

View File

@@ -313,9 +313,9 @@ flow1 = PersistentCounterFlow()
result1 = flow1.kickoff()
print(f"First run result: {result1}")
# Second run - state is automatically loaded
# Second run - pass the ID to load the persisted state
flow2 = PersistentCounterFlow()
result2 = flow2.kickoff()
result2 = flow2.kickoff(inputs={"id": flow1.state.id})
print(f"Second run result: {result2}") # Will be higher due to persisted state
```

View File

@@ -805,7 +805,6 @@ The tables below show a representative sample of current top-performing models a
Begin with well-established models like **GPT-4.1**, **Claude 3.7 Sonnet**, or **Gemini 2.0 Flash** that offer good performance across multiple dimensions and have extensive real-world validation.
</Step>
{" "}
<Step title="Identify Specialized Needs">
Determine if your crew has specific requirements (coding, reasoning, speed)
that would benefit from specialized models like **Claude 4 Sonnet** for
@@ -813,7 +812,6 @@ The tables below show a representative sample of current top-performing models a
consider fast inference providers like **Groq** alongside model selection.
</Step>
{" "}
<Step title="Implement Multi-Model Strategy">
Use different models for different agents based on their roles.
High-capability models for managers and complex tasks, efficient models for

View File

@@ -54,6 +54,14 @@ These tools enable your agents to search the web, research topics, and find info
Extract structured content from web pages using the Tavily API.
</Card>
<Card title="Tavily Research Tool" icon="flask" href="/en/tools/search-research/tavilyresearchtool">
Run multi-step research tasks and get cited reports using the Tavily Research API.
</Card>
<Card title="Tavily Get Research Tool" icon="clipboard-list" href="/en/tools/search-research/tavilygetresearchtool">
Retrieve the status and results of an existing Tavily research task.
</Card>
<Card title="Arxiv Paper Tool" icon="box-archive" href="/en/tools/search-research/arxivpapertool">
Search arXiv and optionally download PDFs.
</Card>
@@ -76,7 +84,15 @@ These tools enable your agents to search the web, research topics, and find info
- **Academic Research**: Find scholarly articles and technical papers
```python
from crewai_tools import SerperDevTool, GitHubSearchTool, YoutubeVideoSearchTool, TavilySearchTool, TavilyExtractorTool
from crewai_tools import (
GitHubSearchTool,
SerperDevTool,
TavilyExtractorTool,
TavilyGetResearchTool,
TavilyResearchTool,
TavilySearchTool,
YoutubeVideoSearchTool,
)
# Create research tools
web_search = SerperDevTool()
@@ -84,11 +100,21 @@ code_search = GitHubSearchTool()
video_research = YoutubeVideoSearchTool()
tavily_search = TavilySearchTool()
content_extractor = TavilyExtractorTool()
tavily_research = TavilyResearchTool()
tavily_get_research = TavilyGetResearchTool()
# Add to your agent
agent = Agent(
role="Research Analyst",
tools=[web_search, code_search, video_research, tavily_search, content_extractor],
tools=[
web_search,
code_search,
video_research,
tavily_search,
content_extractor,
tavily_research,
tavily_get_research,
],
goal="Gather comprehensive information on any topic"
)
```

View File

@@ -0,0 +1,85 @@
---
title: "Tavily Get Research Tool"
description: "Retrieve the status and results of an existing Tavily research task"
icon: "clipboard-list"
mode: "wide"
---
The `TavilyGetResearchTool` lets CrewAI agents check an existing Tavily research task by `request_id`. Use it when a research task was started earlier and you need to retrieve its current status or final results.
If you need to start a new research job, use the [Tavily Research Tool](/en/tools/search-research/tavilyresearchtool). This tool is specifically for looking up an existing Tavily research request after you already have its `request_id`.
## Installation
To use the `TavilyGetResearchTool`, install the `tavily-python` library alongside `crewai-tools`:
```shell
uv add 'crewai[tools]' tavily-python
```
## Environment Variables
Set your Tavily API key:
```bash
export TAVILY_API_KEY='your_tavily_api_key'
```
Get an API key at [https://app.tavily.com/](https://app.tavily.com/) (sign up, then create a key).
## Example Usage
```python
from crewai_tools import TavilyGetResearchTool
tavily_get_research_tool = TavilyGetResearchTool()
status_result = tavily_get_research_tool.run(
request_id="your-research-request-id"
)
print(status_result)
```
## Common Workflow
Use `TavilyGetResearchTool` when your application or another service has already created a Tavily research task and saved its `request_id`.
Typical cases include:
- Polling for completion after kicking off research in a background job.
- Looking up the latest status of a long-running research task.
- Fetching final research output from a previously created Tavily request.
## Configuration Options
The `TavilyGetResearchTool` accepts the following argument when calling the `run` method:
- `request_id` (str): **Required.** The existing Tavily research request ID to retrieve.
## Async Usage
Use `_arun` when your application is already running inside an async event loop:
```python
from crewai_tools import TavilyGetResearchTool
tavily_get_research_tool = TavilyGetResearchTool()
status_result = await tavily_get_research_tool._arun(
request_id="your-research-request-id"
)
```
## Features
- **Research status retrieval**: Fetch the current status of an existing Tavily research task.
- **Result retrieval**: Return available research output once Tavily has completed the task.
- **Sync and async**: Use either `_run`/`run` or `_arun` depending on your application's runtime.
- **JSON output**: Returns Tavily responses as formatted JSON strings.
## Response Format
The tool returns a JSON string containing the current research task status and any available results from Tavily. The exact response shape depends on the task state returned by Tavily, so incomplete tasks may return status information before the final research output is available.
Refer to the [Tavily API documentation](https://docs.tavily.com/) for full details on the Research API.

View File

@@ -4,6 +4,86 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 5월 19일">
## v1.14.5
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5)
## 변경 사항
### 기능
- `CrewAgentExecutor` 사용 중단, 기본 Crew 에이전트를 `AgentExecutor`로 설정
- Daytona 샌드박스 도구 개선
- `restore_from_state_id` 시작 매개변수 추가
- `ExaSearchTool`에 하이라이트 추가, 이름을 `EXASearchTool`에서 변경
### 버그 수정
- `git.py`에서 `cached_property`를 사용하여 메모리 누수 수정
- `available_functions`가 없을 때 스트리밍 도구 호출 표시
- 추적을 위한 `skills` 로딩 이벤트 보장
- 상태 엔드포인트 경로를 `/{kickoff_id}/status`에서 `/status/{kickoff_id}`로 수정
- pt-BR 첫 흐름 가이드에서 누락된 코드 블록 복원
- `result_as_answer`가 후크 블록이나 오류 메시지를 최종 답변으로 반환하지 않도록 방지
- 비동기 배치 플러시 간 작업 출력 보존
- 항상 finally 블록에서 `task.output_pydantic` 복원
- `convert_to_model`에서 `BaseModel` 입력 처리
### 문서화
- v1.14.5에 대한 변경 로그 및 버전 업데이트
- OSS 업그레이드 및 Crew-투-흐름 마이그레이션 가이드 추가
- 개발 도구를 위한 추가 환경 변수 문서화
- `TavilyGetResearch`에 대한 문서 추가
### 리팩토링
- CLI를 독립형 `crewai-cli` 패키지로 추출
## 기여자
@NIK-TIGER-BILL, @akaKuruma, @cgoeppinger, @github-actions[bot], @greysonlalonde, @heitorado, @irfaan101, @iris-clawd, @lorenzejay, @manisrinivasan2k1, @minasami-pr, @mislavivanda, @theCyberTech, @theishangoswami, @wishhyt
</Update>
<Update label="2026년 5월 18일">
## v1.14.5a7
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a7)
## 변경 사항
### 문서
- v1.14.5a6의 변경 로그 및 버전 업데이트
### 주요 변경 사항
- function_calling_llm 필드 사용 중단
## 기여자
@greysonlalonde, @heitorado
</Update>
<Update label="2026년 5월 15일">
## v1.14.5a6
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a6)
## 변경 사항
### 버그 수정
- available_functions가 없을 때 스트리밍 도구 호출 수정
- GHSA-3644-q5cj-c5c7 문제를 해결하기 위해 langsmith 의존성을 버전 >=0.8.0으로 업데이트
- 브라질 포르투갈어 문서에서 번역되지 않은 코드 블록 자리 표시자 해결
### 문서
- TavilyGetResearch에 대한 문서 추가
- v1.14.5a5에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde, @heitorado, @iris-clawd, @lorenzejay, @manisrinivasan2k1
</Update>
<Update label="2026년 5월 13일">
## v1.14.5a5

View File

@@ -29,6 +29,7 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

View File

@@ -145,7 +145,6 @@ LLM 연결과 기본 설정을 구성했다면 이제 Crew Studio 사용을 시
</Step>
{" "}
<Step title="질문에 답하기">
crew assistant가 요구 사항을 구체화할 수 있도록 하는 추가 질문에 답변하세요.
</Step>
@@ -159,12 +158,10 @@ LLM 연결과 기본 설정을 구성했다면 이제 Crew Studio 사용을 시
</Step>
{" "}
<Step title="승인 또는 수정">
계획을 승인하거나 필요하다면 변경을 요청하세요.
</Step>
{" "}
<Step title="다운로드 또는 배포">
사용자화를 위해 코드를 다운로드하거나 플랫폼에 직접 배포하세요.
</Step>

View File

@@ -797,7 +797,6 @@ LLM 선택을 최적화하고자 하는 팀을 위해 **CrewAI AMP 플랫폼**
여러 차원에서 우수한 성능을 제공하며 실제 환경에서 광범위하게 검증된 **GPT-4.1**, **Claude 3.7 Sonnet**, **Gemini 2.0 Flash**와 같은 잘 알려진 모델부터 시작하십시오.
</Step>
{" "}
<Step title="특화된 요구 사항 식별">
crew에 코드 작성, reasoning, 속도 등 특정 요구가 있는지 확인하고, 이러한
요구에 부합하는 **Claude 4 Sonnet**(개발용) 또는 **o3**(복잡한 분석용)과 같은
@@ -805,7 +804,6 @@ LLM 선택을 최적화하고자 하는 팀을 위해 **CrewAI AMP 플랫폼**
더불어 **Groq**와 같은 빠른 추론 제공자를 고려할 수 있습니다.
</Step>
{" "}
<Step title="다중 모델 전략 구현">
각 에이전트의 역할에 따라 다양한 모델을 사용하세요. 관리자와 복잡한 작업에는
고성능 모델을, 일상적 운영에는 효율적인 모델을 적용합니다.

View File

@@ -4,6 +4,86 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="19 mai 2026">
## v1.14.5
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5)
## O que Mudou
### Recursos
- Deprecar `CrewAgentExecutor`, definir agentes Crew como `AgentExecutor`
- Melhorar ferramentas do sandbox Daytona
- Adicionar parâmetro de início `restore_from_state_id`
- Adicionar destaques ao `ExaSearchTool`, renomeando de `EXASearchTool`
### Correções de Bugs
- Corrigir vazamento de memória em `git.py` usando `cached_property`
- Exibir chamadas de ferramentas transmitidas quando `available_functions` está ausente
- Garantir eventos de carregamento de `skills` para rastros
- Corrigir caminho do endpoint de status de `/{kickoff_id}/status` para `/status/{kickoff_id}`
- Restaurar bloco de código ausente no guia de primeiro fluxo em pt-BR
- Impedir que `result_as_answer` retorne mensagens de bloqueio de hook ou de erro como resposta final
- Preservar saídas de tarefas durante o descarregamento assíncrono em lote
- Sempre restaurar `task.output_pydantic` no bloco finally
- Lidar com entrada de `BaseModel` em `convert_to_model`
### Documentação
- Atualizar changelog e versão para v1.14.5
- Adicionar guia de migração de atualização OSS & crew-to-flow
- Documentar variáveis de ambiente adicionais para devtools
- Adicionar documentação para `TavilyGetResearch`
### Refatoração
- Extrair CLI para o pacote autônomo `crewai-cli`
## Contribuidores
@NIK-TIGER-BILL, @akaKuruma, @cgoeppinger, @github-actions[bot], @greysonlalonde, @heitorado, @irfaan101, @iris-clawd, @lorenzejay, @manisrinivasan2k1, @minasami-pr, @mislavivanda, @theCyberTech, @theishangoswami, @wishhyt
</Update>
<Update label="18 mai 2026">
## v1.14.5a7
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a7)
## O que Mudou
### Documentação
- Atualizar changelog e versão para v1.14.5a6
### Mudanças Quebradoras
- Depreciar o campo function_calling_llm
## Contributors
@greysonlalonde, @heitorado
</Update>
<Update label="15 mai 2026">
## v1.14.5a6
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a6)
## O que mudou
### Correções de Bugs
- Corrigir chamadas de ferramentas transmitidas quando available_functions está ausente
- Atualizar a dependência langsmith para a versão >=0.8.0 para resolver GHSA-3644-q5cj-c5c7
- Resolver espaços reservados de blocos de código não traduzidos na documentação em português brasileiro
### Documentação
- Adicionar documentação para TavilyGetResearch
- Atualizar changelog e versão para v1.14.5a5
## Contributors
@greysonlalonde, @heitorado, @iris-clawd, @lorenzejay, @manisrinivasan2k1
</Update>
<Update label="13 mai 2026">
## v1.14.5a5

View File

@@ -24,7 +24,63 @@ Os flows permitem que você crie fluxos de trabalho estruturados e orientados po
Vamos criar um Flow simples no qual você usará a OpenAI para gerar uma cidade aleatória em uma tarefa e, em seguida, usará essa cidade para gerar uma curiosidade em outra tarefa.
```python Code
# (O código não é traduzido)
from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"
@start()
def generate_city(self):
print("Starting flow")
# Cada estado do flow recebe automaticamente um ID único
print(f"Flow State ID: {self.state['id']}")
response = completion(
model=self.model,
messages=[
{
"role": "user",
"content": "Return the name of a random city in the world.",
},
],
)
random_city = response["choices"][0]["message"]["content"]
# Armazena a cidade no nosso estado
self.state["city"] = random_city
print(f"Random City: {random_city}")
return random_city
@listen(generate_city)
def generate_fun_fact(self, random_city):
response = completion(
model=self.model,
messages=[
{
"role": "user",
"content": f"Tell me a fun fact about {random_city}",
},
],
)
fun_fact = response["choices"][0]["message"]["content"]
# Armazena a curiosidade no nosso estado
self.state["fun_fact"] = fun_fact
return fun_fact
flow = ExampleFlow()
flow.plot()
result = flow.kickoff()
print(f"Generated fun fact: {result}")
```
Na ilustração acima, criamos um Flow simples que gera uma cidade aleatória usando a OpenAI e depois cria uma curiosidade sobre essa cidade. O Flow consiste em duas tarefas: `generate_city` e `generate_fun_fact`. A tarefa `generate_city` é o ponto de início do Flow, enquanto a tarefa `generate_fun_fact` fica escutando o resultado da tarefa `generate_city`.
@@ -56,12 +112,16 @@ O decorador `@listen()` pode ser usado de várias formas:
1. **Escutando um Método pelo Nome**: Você pode passar o nome do método ao qual deseja escutar como string. Quando esse método concluir, o método ouvinte será chamado.
```python Code
# (O código não é traduzido)
@listen("generate_city")
def generate_fun_fact(self, random_city):
# Implementação
```
2. **Escutando um Método Diretamente**: Você pode passar o próprio método. Quando esse método concluir, o método ouvinte será chamado.
```python Code
# (O código não é traduzido)
@listen(generate_city)
def generate_fun_fact(self, random_city):
# Implementação
```
### Saída de um Flow
@@ -76,7 +136,24 @@ Veja como acessar a saída final:
<CodeGroup>
```python Code
# (O código não é traduzido)
from crewai.flow.flow import Flow, listen, start
class OutputExampleFlow(Flow):
@start()
def first_method(self):
return "Output from first_method"
@listen(first_method)
def second_method(self, first_output):
return f"Second method received: {first_output}"
flow = OutputExampleFlow()
flow.plot("my_flow_plot")
final_output = flow.kickoff()
print("---- Final Output ----")
print(final_output)
```
```text Output
@@ -97,8 +174,34 @@ Além de recuperar a saída final, você pode acessar e atualizar o estado dentr
Veja um exemplo de como atualizar e acessar o estado:
<CodeGroup>
```python Code
# (O código não é traduzido)
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
class ExampleState(BaseModel):
counter: int = 0
message: str = ""
class StateExampleFlow(Flow[ExampleState]):
@start()
def first_method(self):
self.state.message = "Hello from first_method"
self.state.counter += 1
@listen(first_method)
def second_method(self):
self.state.message += " - updated by second_method"
self.state.counter += 1
return self.state.message
flow = StateExampleFlow()
flow.plot("my_flow_plot")
final_output = flow.kickoff()
print(f"Final Output: {final_output}")
print("Final State:")
print(flow.state)
```
```text Output
@@ -128,7 +231,33 @@ Essa abordagem oferece flexibilidade, permitindo que o desenvolvedor adicione ou
Mesmo com estados não estruturados, os flows do CrewAI geram e mantêm automaticamente um identificador único (UUID) para cada instância de estado.
```python Code
# (O código não é traduzido)
from crewai.flow.flow import Flow, listen, start
class UnstructuredExampleFlow(Flow):
@start()
def first_method(self):
# O estado inclui automaticamente um campo 'id'
print(f"State ID: {self.state['id']}")
self.state['counter'] = 0
self.state['message'] = "Hello from structured flow"
@listen(first_method)
def second_method(self):
self.state['counter'] += 1
self.state['message'] += " - updated"
@listen(second_method)
def third_method(self):
self.state['counter'] += 1
self.state['message'] += " - updated again"
print(f"State after third_method: {self.state}")
flow = UnstructuredExampleFlow()
flow.plot("my_flow_plot")
flow.kickoff()
```
![Flow Visual image](/images/crewai-flow-3.png)
@@ -148,7 +277,39 @@ Ao usar modelos como o `BaseModel` da Pydantic, os desenvolvedores podem definir
Cada estado nos flows do CrewAI recebe automaticamente um identificador único (UUID) para ajudar no rastreamento e gerenciamento. Esse ID é gerado e mantido automaticamente pelo sistema de flows.
```python Code
# (O código não é traduzido)
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
class ExampleState(BaseModel):
# Nota: o campo 'id' é adicionado automaticamente a todos os estados
counter: int = 0
message: str = ""
class StructuredExampleFlow(Flow[ExampleState]):
@start()
def first_method(self):
# Acesse o ID gerado automaticamente, se necessário
print(f"State ID: {self.state.id}")
self.state.message = "Hello from structured flow"
@listen(first_method)
def second_method(self):
self.state.counter += 1
self.state.message += " - updated"
@listen(second_method)
def third_method(self):
self.state.counter += 1
self.state.message += " - updated again"
print(f"State after third_method: {self.state}")
flow = StructuredExampleFlow()
flow.kickoff()
```
![Flow Visual image](/images/crewai-flow-3.png)
@@ -182,7 +343,19 @@ O decorador @persist permite a persistência automática do estado nos flows do
Quando aplicado no nível da classe, o decorador @persist garante a persistência automática de todos os estados dos métodos do flow:
```python
# (O código não é traduzido)
@persist # Usa SQLiteFlowPersistence por padrão
class MyFlow(Flow[MyState]):
@start()
def initialize_flow(self):
# Este método terá seu estado persistido automaticamente
self.state.counter = 1
print("Initialized flow. State ID:", self.state.id)
@listen(initialize_flow)
def next_step(self):
# O estado (incluindo self.state.id) é recarregado automaticamente
self.state.counter += 1
print("Flow state is persisted. Counter:", self.state.counter)
```
### Persistência no Nível de Método
@@ -190,7 +363,14 @@ Quando aplicado no nível da classe, o decorador @persist garante a persistênci
Para um controle mais granular, você pode aplicar @persist em métodos específicos:
```python
# (O código não é traduzido)
class AnotherFlow(Flow[dict]):
@persist # Persiste apenas o estado deste método
@start()
def begin(self):
if "runs" not in self.state:
self.state["runs"] = 0
self.state["runs"] += 1
print("Method-level persisted runs:", self.state["runs"])
```
### Forking de Estado Persistido
@@ -282,8 +462,29 @@ A arquitetura de persistência enfatiza precisão técnica e opções de persona
A função `or_` nos flows permite escutar múltiplos métodos e acionar o método ouvinte quando qualquer um dos métodos especificados gerar uma saída.
<CodeGroup>
```python Code
# (O código não é traduzido)
from crewai.flow.flow import Flow, listen, or_, start
class OrExampleFlow(Flow):
@start()
def start_method(self):
return "Hello from the start method"
@listen(start_method)
def second_method(self):
return "Hello from the second method"
@listen(or_(start_method, second_method))
def logger(self, result):
print(f"Logger: {result}")
flow = OrExampleFlow()
flow.plot("my_flow_plot")
flow.kickoff()
```
```text Output
@@ -302,8 +503,28 @@ A função `or_` serve para escutar vários métodos e disparar o método ouvint
A função `and_` nos flows permite escutar múltiplos métodos e acionar o método ouvinte apenas quando todos os métodos especificados emitirem uma saída.
<CodeGroup>
```python Code
# (O código não é traduzido)
from crewai.flow.flow import Flow, and_, listen, start
class AndExampleFlow(Flow):
@start()
def start_method(self):
self.state["greeting"] = "Hello from the start method"
@listen(start_method)
def second_method(self):
self.state["joke"] = "What do computers eat? Microchips."
@listen(and_(start_method, second_method))
def logger(self):
print("---- Logger ----")
print(self.state)
flow = AndExampleFlow()
flow.plot()
flow.kickoff()
```
```text Output
@@ -323,8 +544,42 @@ O decorador `@router()` nos flows permite definir lógica de roteamento condicio
Você pode especificar diferentes rotas conforme a saída do método, permitindo controlar o fluxo de execução de forma dinâmica.
<CodeGroup>
```python Code
# (O código não é traduzido)
import random
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
class ExampleState(BaseModel):
success_flag: bool = False
class RouterFlow(Flow[ExampleState]):
@start()
def start_method(self):
print("Starting the structured flow")
random_boolean = random.choice([True, False])
self.state.success_flag = random_boolean
@router(start_method)
def second_method(self):
if self.state.success_flag:
return "success"
else:
return "failed"
@listen("success")
def third_method(self):
print("Third method running")
@listen("failed")
def fourth_method(self):
print("Fourth method running")
flow = RouterFlow()
flow.plot("my_flow_plot")
flow.kickoff()
```
```text Output
@@ -401,7 +656,105 @@ Para um guia completo sobre feedback humano em flows, incluindo feedback assínc
Os agentes podem ser integrados facilmente aos seus flows, oferecendo uma alternativa leve às crews completas quando você precisar executar tarefas simples e focadas. Veja um exemplo de como utilizar um agente em um flow para realizar uma pesquisa de mercado:
```python
# (O código não é traduzido)
import asyncio
from typing import Any, Dict, List
from crewai_tools import SerperDevTool
from pydantic import BaseModel, Field
from crewai.agent import Agent
from crewai.flow.flow import Flow, listen, start
# Define um formato de saída estruturado
class MarketAnalysis(BaseModel):
key_trends: List[str] = Field(description="List of identified market trends")
market_size: str = Field(description="Estimated market size")
competitors: List[str] = Field(description="Major competitors in the space")
# Define o estado do flow
class MarketResearchState(BaseModel):
product: str = ""
analysis: MarketAnalysis | None = None
# Cria uma classe de flow
class MarketResearchFlow(Flow[MarketResearchState]):
@start()
def initialize_research(self) -> Dict[str, Any]:
print(f"Starting market research for {self.state.product}")
return {"product": self.state.product}
@listen(initialize_research)
async def analyze_market(self) -> Dict[str, Any]:
# Cria um agente para pesquisa de mercado
analyst = Agent(
role="Market Research Analyst",
goal=f"Analyze the market for {self.state.product}",
backstory="You are an experienced market analyst with expertise in "
"identifying market trends and opportunities.",
tools=[SerperDevTool()],
verbose=True,
)
# Define a consulta de pesquisa
query = f"""
Research the market for {self.state.product}. Include:
1. Key market trends
2. Market size
3. Major competitors
Format your response according to the specified structure.
"""
# Executa a análise com formato de saída estruturado
result = await analyst.kickoff_async(query, response_format=MarketAnalysis)
if result.pydantic:
print("result", result.pydantic)
else:
print("result", result)
# Retorna a análise para atualizar o estado
return {"analysis": result.pydantic}
@listen(analyze_market)
def present_results(self, analysis) -> None:
print("\nMarket Analysis Results")
print("=====================")
if isinstance(analysis, dict):
# Se recebemos um dict com a chave 'analysis', extrai o objeto de análise real
market_analysis = analysis.get("analysis")
else:
market_analysis = analysis
if market_analysis and isinstance(market_analysis, MarketAnalysis):
print("\nKey Market Trends:")
for trend in market_analysis.key_trends:
print(f"- {trend}")
print(f"\nMarket Size: {market_analysis.market_size}")
print("\nMajor Competitors:")
for competitor in market_analysis.competitors:
print(f"- {competitor}")
else:
print("No structured analysis data available.")
print("Raw analysis:", analysis)
# Exemplo de uso
async def run_flow():
flow = MarketResearchFlow()
flow.plot("MarketResearchFlowPlot")
result = await flow.kickoff_async(inputs={"product": "AI-powered chatbots"})
return result
# Executa o flow
if __name__ == "__main__":
asyncio.run(run_flow())
```
![Flow Visual image](/images/crewai-flow-7.png)
@@ -463,7 +816,50 @@ No arquivo `main.py`, você cria seu flow e conecta as crews. É possível defin
Veja um exemplo de como conectar a `poem_crew` no arquivo `main.py`:
```python Code
# (O código não é traduzido)
#!/usr/bin/env python
from random import randint
from pydantic import BaseModel
from crewai.flow.flow import Flow, listen, start
from .crews.poem_crew.poem_crew import PoemCrew
class PoemState(BaseModel):
sentence_count: int = 1
poem: str = ""
class PoemFlow(Flow[PoemState]):
@start()
def generate_sentence_count(self):
print("Generating sentence count")
self.state.sentence_count = randint(1, 5)
@listen(generate_sentence_count)
def generate_poem(self):
print("Generating poem")
result = PoemCrew().crew().kickoff(inputs={"sentence_count": self.state.sentence_count})
print("Poem generated", result.raw)
self.state.poem = result.raw
@listen(generate_poem)
def save_poem(self):
print("Saving poem")
with open("poem.txt", "w") as f:
f.write(self.state.poem)
def kickoff():
poem_flow = PoemFlow()
poem_flow.kickoff()
def plot():
poem_flow = PoemFlow()
poem_flow.plot("PoemFlowPlot")
if __name__ == "__main__":
kickoff()
plot()
```
Neste exemplo, a classe `PoemFlow` define um fluxo que gera a quantidade de frases, usa a `PoemCrew` para gerar um poema e, depois, salva o poema em um arquivo. O flow inicia com o método `kickoff()`, e o gráfico é gerado pelo método `plot()`.
@@ -515,7 +911,8 @@ O CrewAI oferece duas formas práticas de gerar plots dos seus flows:
Se estiver trabalhando diretamente com uma instância do flow, basta chamar o método `plot()` do objeto. Isso criará um arquivo HTML com o plot interativo do seu flow.
```python Code
# (O código não é traduzido)
# Considerando que você já tem uma instância do flow
flow.plot("my_flow_plot")
```
Esse comando gera um arquivo chamado `my_flow_plot.html` no diretório atual. Abra esse arquivo em um navegador para visualizar o plot interativo.

View File

@@ -146,7 +146,6 @@ Veja um fluxo de trabalho típico para criação de um crew com o Crew Studio:
</Step>
{" "}
<Step title="Responder Perguntas">
Responda às perguntas de esclarecimento do Crew Assistant para refinar seus
requisitos.
@@ -161,12 +160,10 @@ Veja um fluxo de trabalho típico para criação de um crew com o Crew Studio:
</Step>
{" "}
<Step title="Aprovar ou Modificar">
Aprove o plano ou solicite alterações, se necessário.
</Step>
{" "}
<Step title="Baixar ou Fazer Deploy">
Baixe o código para personalização ou faça o deploy diretamente na plataforma.
</Step>

View File

@@ -63,7 +63,60 @@ Com estado não estruturado:
Veja um exemplo simples de gerenciamento de estado não estruturado:
```python
# código não traduzido
from crewai.flow.flow import Flow, listen, start
class UnstructuredStateFlow(Flow):
@start()
def initialize_data(self):
print("Initializing flow data")
# Adiciona pares chave-valor ao estado
self.state["user_name"] = "Alex"
self.state["preferences"] = {
"theme": "dark",
"language": "English"
}
self.state["items"] = []
# O estado do flow recebe automaticamente um ID único
print(f"Flow ID: {self.state['id']}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Previous step returned: {previous_result}")
# Acessa e modifica o estado
user = self.state["user_name"]
print(f"Processing data for {user}")
# Adiciona itens a uma lista no estado
self.state["items"].append("item1")
self.state["items"].append("item2")
# Adiciona um novo par chave-valor
self.state["processed"] = True
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Acessa múltiplos valores do estado
user = self.state["user_name"]
theme = self.state["preferences"]["theme"]
items = self.state["items"]
processed = self.state.get("processed", False)
summary = f"User {user} has {len(items)} items with {theme} theme. "
summary += "Data is processed." if processed else "Data is not processed."
return summary
# Executa o flow
flow = UnstructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
```
### Quando Usar Estado Não Estruturado
@@ -94,7 +147,63 @@ Ao utilizar estado estruturado:
Veja como implementar o gerenciamento de estado estruturado:
```python
# código não traduzido
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel, Field
from typing import List, Dict, Optional
# Define o modelo de estado
class UserPreferences(BaseModel):
theme: str = "light"
language: str = "English"
class AppState(BaseModel):
user_name: str = ""
preferences: UserPreferences = UserPreferences()
items: List[str] = []
processed: bool = False
completion_percentage: float = 0.0
# Cria um flow com estado tipado
class StructuredStateFlow(Flow[AppState]):
@start()
def initialize_data(self):
print("Initializing flow data")
# Define valores do estado (com checagem de tipo)
self.state.user_name = "Taylor"
self.state.preferences.theme = "dark"
# O campo ID está disponível automaticamente
print(f"Flow ID: {self.state.id}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Processing data for {self.state.user_name}")
# Modifica o estado (com checagem de tipo)
self.state.items.append("item1")
self.state.items.append("item2")
self.state.processed = True
self.state.completion_percentage = 50.0
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Acessa o estado (com autocompletar)
summary = f"User {self.state.user_name} has {len(self.state.items)} items "
summary += f"with {self.state.preferences.theme} theme. "
summary += "Data is processed." if self.state.processed else "Data is not processed."
summary += f" Completion: {self.state.completion_percentage}%"
return summary
# Executa o flow
flow = StructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
```
### Benefícios do Estado Estruturado
@@ -138,7 +247,29 @@ Independente de você usar estado estruturado ou não estruturado, é possível
Métodos do flow podem retornar valores que serão passados como argumento para métodos listeners:
```python
# código não traduzido
from crewai.flow.flow import Flow, listen, start
class DataPassingFlow(Flow):
@start()
def generate_data(self):
# Este valor de retorno será passado para os métodos listeners
return "Generated data"
@listen(generate_data)
def process_data(self, data_from_previous_step):
print(f"Received: {data_from_previous_step}")
# Você pode modificar os dados e repassá-los adiante
processed_data = f"{data_from_previous_step} - processed"
# Também atualiza o estado
self.state["last_processed"] = processed_data
return processed_data
@listen(process_data)
def finalize_data(self, processed_data):
print(f"Received processed data: {processed_data}")
# Acessa tanto os dados passados quanto o estado
last_processed = self.state.get("last_processed", "")
return f"Final: {processed_data} (from state: {last_processed})"
```
Esse padrão permite combinar passagem de dados direta com atualizações de estado para obter máxima flexibilidade.
@@ -156,7 +287,36 @@ O decorador `@persist()` automatiza a persistência de estado, salvando o estado
Ao aplicar em nível de classe, `@persist()` salva o estado após cada execução de método:
```python
# código não traduzido
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
from pydantic import BaseModel
class CounterState(BaseModel):
value: int = 0
@persist() # Aplica à classe inteira do flow
class PersistentCounterFlow(Flow[CounterState]):
@start()
def increment(self):
self.state.value += 1
print(f"Incremented to {self.state.value}")
return self.state.value
@listen(increment)
def double(self, value):
self.state.value = value * 2
print(f"Doubled to {self.state.value}")
return self.state.value
# Primeira execução
flow1 = PersistentCounterFlow()
result1 = flow1.kickoff()
print(f"First run result: {result1}")
# Segunda execução - passa o ID para carregar o estado persistido
flow2 = PersistentCounterFlow()
result2 = flow2.kickoff(inputs={"id": flow1.state.id})
print(f"Second run result: {result2}") # Será maior devido ao estado persistido
```
#### Persistência em Nível de Método
@@ -164,7 +324,26 @@ Ao aplicar em nível de classe, `@persist()` salva o estado após cada execuçã
Para mais controle, você pode aplicar `@persist()` em métodos específicos:
```python
# código não traduzido
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
class SelectivePersistFlow(Flow):
@start()
def first_step(self):
self.state["count"] = 1
return "First step"
@persist() # Persiste apenas após este método
@listen(first_step)
def important_step(self, prev_result):
self.state["count"] += 1
self.state["important_data"] = "This will be persisted"
return "Important step completed"
@listen(important_step)
def final_step(self, prev_result):
self.state["count"] += 1
return f"Complete with count {self.state['count']}"
```
#### Forking de Estado Persistido
@@ -216,7 +395,45 @@ Notas sobre o comportamento:
Você pode usar o estado para implementar lógicas condicionais complexas em seus flows:
```python
# código não traduzido
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
class PaymentState(BaseModel):
amount: float = 0.0
is_approved: bool = False
retry_count: int = 0
class PaymentFlow(Flow[PaymentState]):
@start()
def process_payment(self):
# Simula o processamento do pagamento
self.state.amount = 100.0
self.state.is_approved = self.state.amount < 1000
return "Payment processed"
@router(process_payment)
def check_approval(self, previous_result):
if self.state.is_approved:
return "approved"
elif self.state.retry_count < 3:
return "retry"
else:
return "rejected"
@listen("approved")
def handle_approval(self):
return f"Payment of ${self.state.amount} approved!"
@listen("retry")
def handle_retry(self):
self.state.retry_count += 1
print(f"Retrying payment (attempt {self.state.retry_count})...")
# Aqui poderia ser implementada a lógica de retry
return "Retry initiated"
@listen("rejected")
def handle_rejection(self):
return f"Payment of ${self.state.amount} rejected after {self.state.retry_count} retries."
```
### Manipulações Complexas de Estado
@@ -224,7 +441,60 @@ Você pode usar o estado para implementar lógicas condicionais complexas em seu
Para transformar estados complexos, você pode criar métodos dedicados:
```python
# código não traduzido
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
from typing import List, Dict
class UserData(BaseModel):
name: str
active: bool = True
login_count: int = 0
class ComplexState(BaseModel):
users: Dict[str, UserData] = {}
active_user_count: int = 0
class TransformationFlow(Flow[ComplexState]):
@start()
def initialize(self):
# Adiciona alguns usuários
self.add_user("alice", "Alice")
self.add_user("bob", "Bob")
self.add_user("charlie", "Charlie")
return "Initialized"
@listen(initialize)
def process_users(self, _):
# Incrementa contagens de login
for user_id in self.state.users:
self.increment_login(user_id)
# Desativa um usuário
self.deactivate_user("bob")
# Atualiza a contagem de ativos
self.update_active_count()
return f"Processed {len(self.state.users)} users"
# Métodos auxiliares para transformações de estado
def add_user(self, user_id: str, name: str):
self.state.users[user_id] = UserData(name=name)
self.update_active_count()
def increment_login(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].login_count += 1
def deactivate_user(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].active = False
self.update_active_count()
def update_active_count(self):
self.state.active_user_count = sum(
1 for user in self.state.users.values() if user.active
)
```
Esse padrão de criar métodos auxiliares mantém seus métodos de flow limpos, enquanto permite manipulações complexas de estado.
@@ -238,7 +508,71 @@ Um dos padrões mais poderosos na CrewAI é combinar o gerenciamento de estado d
Você pode usar o estado do flow para parametrizar crews:
```python
# código não traduzido
from crewai.flow.flow import Flow, listen, start
from crewai import Agent, Crew, Process, Task
from pydantic import BaseModel
class ResearchState(BaseModel):
topic: str = ""
depth: str = "medium"
results: str = ""
class ResearchFlow(Flow[ResearchState]):
@start()
def get_parameters(self):
# Em uma aplicação real, isso pode vir da entrada do usuário
self.state.topic = "Artificial Intelligence Ethics"
self.state.depth = "deep"
return "Parameters set"
@listen(get_parameters)
def execute_research(self, _):
# Cria os agentes
researcher = Agent(
role="Research Specialist",
goal=f"Research {self.state.topic} in {self.state.depth} detail",
backstory="You are an expert researcher with a talent for finding accurate information."
)
writer = Agent(
role="Content Writer",
goal="Transform research into clear, engaging content",
backstory="You excel at communicating complex ideas clearly and concisely."
)
# Cria as tarefas
research_task = Task(
description=f"Research {self.state.topic} with {self.state.depth} analysis",
expected_output="Comprehensive research notes in markdown format",
agent=researcher
)
writing_task = Task(
description=f"Create a summary on {self.state.topic} based on the research",
expected_output="Well-written article in markdown format",
agent=writer,
context=[research_task]
)
# Cria e executa a crew
research_crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process=Process.sequential,
verbose=True
)
# Executa a crew e armazena o resultado no estado
result = research_crew.kickoff()
self.state.results = result.raw
return "Research completed"
@listen(execute_research)
def summarize_results(self, _):
# Acessa os resultados armazenados
result_length = len(self.state.results)
return f"Research on {self.state.topic} completed with {result_length} characters of results."
```
### Manipulando Saídas de Crews no Estado
@@ -246,7 +580,21 @@ Você pode usar o estado do flow para parametrizar crews:
Quando um crew finaliza, é possível processar sua saída e armazená-la no estado do flow:
```python
# código não traduzido
@listen(execute_crew)
def process_crew_results(self, _):
# Faz parsing dos resultados brutos (assumindo saída em JSON)
import json
try:
results_dict = json.loads(self.state.raw_results)
self.state.processed_results = {
"title": results_dict.get("title", ""),
"main_points": results_dict.get("main_points", []),
"conclusion": results_dict.get("conclusion", "")
}
return "Results processed successfully"
except json.JSONDecodeError:
self.state.error = "Failed to parse crew results as JSON"
return "Error processing results"
```
## Boas Práticas para Gerenciamento de Estado
@@ -256,7 +604,19 @@ Quando um crew finaliza, é possível processar sua saída e armazená-la no est
Projete seu estado para conter somente o necessário:
```python
# Exemplo não traduzido
# Abrangente demais
class BloatedState(BaseModel):
user_data: Dict = {}
system_settings: Dict = {}
temporary_calculations: List = []
debug_info: Dict = {}
# ...muitos outros campos
# Melhor: estado focado
class FocusedState(BaseModel):
user_id: str
preferences: Dict[str, str]
completion_status: Dict[str, bool]
```
### 2. Use Estado Estruturado em Flows Complexos
@@ -264,7 +624,23 @@ Projete seu estado para conter somente o necessário:
À medida que seus flows evoluem em complexidade, o estado estruturado se torna cada vez mais valioso:
```python
# Exemplo não traduzido
# Flow simples pode usar estado não estruturado
class SimpleGreetingFlow(Flow):
@start()
def greet(self):
self.state["name"] = "World"
return f"Hello, {self.state['name']}!"
# Flow complexo se beneficia de estado estruturado
class UserRegistrationState(BaseModel):
username: str
email: str
verification_status: bool = False
registration_date: datetime = Field(default_factory=datetime.now)
last_login: Optional[datetime] = None
class RegistrationFlow(Flow[UserRegistrationState]):
# Métodos com acesso ao estado fortemente tipado
```
### 3. Documente Transições de Estado
@@ -272,7 +648,18 @@ Projete seu estado para conter somente o necessário:
Para flows complexos, documente como o estado muda ao longo da execução:
```python
# Exemplo não traduzido
@start()
def initialize_order(self):
"""
Initialize order state with empty values.
State before: {}
State after: {order_id: str, items: [], status: 'new'}
"""
self.state.order_id = str(uuid.uuid4())
self.state.items = []
self.state.status = "new"
return "Order initialized"
```
### 4. Trate Erros de Estado de Forma Elegante
@@ -280,7 +667,18 @@ Para flows complexos, documente como o estado muda ao longo da execução:
Implemente tratamento de erros ao acessar o estado:
```python
# Exemplo não traduzido
@listen(previous_step)
def process_data(self, _):
try:
# Tenta acessar um valor que pode não existir
user_preference = self.state.preferences.get("theme", "default")
except (AttributeError, KeyError):
# Trata o erro de forma elegante
self.state.errors = self.state.get("errors", [])
self.state.errors.append("Failed to access preferences")
user_preference = "default"
return f"Used preference: {user_preference}"
```
### 5. Use o Estado Para Acompanhar o Progresso
@@ -288,7 +686,30 @@ Implemente tratamento de erros ao acessar o estado:
Aproveite o estado para monitorar o progresso em flows de longa duração:
```python
# Exemplo não traduzido
class ProgressTrackingFlow(Flow):
@start()
def initialize(self):
self.state["total_steps"] = 3
self.state["current_step"] = 0
self.state["progress"] = 0.0
self.update_progress()
return "Initialized"
def update_progress(self):
"""Helper method to calculate and update progress"""
if self.state.get("total_steps", 0) > 0:
self.state["progress"] = (self.state.get("current_step", 0) /
self.state["total_steps"]) * 100
print(f"Progress: {self.state['progress']:.1f}%")
@listen(initialize)
def step_one(self, _):
# Realiza o trabalho...
self.state["current_step"] = 1
self.update_progress()
return "Step 1 complete"
# Etapas adicionais...
```
### 6. Prefira Operações Imutáveis Quando Possível
@@ -296,7 +717,22 @@ Aproveite o estado para monitorar o progresso em flows de longa duração:
Especialmente com estado estruturado, prefira operações imutáveis para maior clareza:
```python
# Exemplo não traduzido
# Em vez de modificar listas no local:
self.state.items.append(new_item) # Operação mutável
# Considere criar um novo estado:
from pydantic import BaseModel
from typing import List
class ItemState(BaseModel):
items: List[str] = []
class ImmutableFlow(Flow[ItemState]):
@start()
def add_item(self):
# Cria uma nova lista com o item adicionado
self.state.items = [*self.state.items, "new item"]
return "Item added"
```
## Depurando o Estado do Flow
@@ -306,7 +742,24 @@ Especialmente com estado estruturado, prefira operações imutáveis para maior
Ao desenvolver, adicione logs para acompanhar mudanças no estado:
```python
# Exemplo não traduzido
import logging
logging.basicConfig(level=logging.INFO)
class LoggingFlow(Flow):
def log_state(self, step_name):
logging.info(f"State after {step_name}: {self.state}")
@start()
def initialize(self):
self.state["counter"] = 0
self.log_state("initialize")
return "Initialized"
@listen(initialize)
def increment(self, _):
self.state["counter"] += 1
self.log_state("increment")
return f"Incremented to {self.state['counter']}"
```
### Visualizando o Estado
@@ -314,7 +767,30 @@ Ao desenvolver, adicione logs para acompanhar mudanças no estado:
Você pode adicionar métodos para visualizar seu estado durante o debug:
```python
# Exemplo não traduzido
def visualize_state(self):
"""Create a simple visualization of the current state"""
import json
from rich.console import Console
from rich.panel import Panel
console = Console()
if hasattr(self.state, "model_dump"):
# Pydantic v2
state_dict = self.state.model_dump()
elif hasattr(self.state, "dict"):
# Pydantic v1
state_dict = self.state.dict()
else:
# Estado não estruturado
state_dict = dict(self.state)
# Remove o id para uma saída mais limpa
if "id" in state_dict:
state_dict.pop("id")
state_json = json.dumps(state_dict, indent=2, default=str)
console.print(Panel(state_json, title="Current Flow State"))
```
## Conclusão

View File

@@ -797,7 +797,6 @@ As tabelas abaixo mostram uma amostra dos modelos de maior destaque em cada cate
Inicie com opções consagradas como **GPT-4.1**, **Claude 3.7 Sonnet** ou **Gemini 2.0 Flash**, que oferecem bom desempenho e ampla validação.
</Step>
{" "}
<Step title="Identifique Demandas Especializadas">
Descubra se sua crew possui requisitos específicos (código, raciocínio,
velocidade) que justifiquem modelos como **Claude 4 Sonnet** para
@@ -805,7 +804,6 @@ As tabelas abaixo mostram uma amostra dos modelos de maior destaque em cada cate
velocidade, considere Groq aliado à seleção do modelo.
</Step>
{" "}
<Step title="Implemente Estratégia Multi-Modelo">
Use modelos diferentes para agentes distintos conforme o papel. Modelos de
alta capacidade para managers e tarefas complexas, eficientes para rotinas.

View File

@@ -8,7 +8,7 @@ authors = [
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai-core==1.14.5a5",
"crewai-core==1.14.5",
"click~=8.1.7",
"pydantic>=2.11.9,<2.13",
"pydantic-settings~=2.10.1",

View File

@@ -1 +1 @@
__version__ = "1.14.5a5"
__version__ = "1.14.5"

View File

@@ -1,5 +1,6 @@
from typing import Any
from crewai_core.plus_api import CreateCrewPayload
from rich.console import Console
from crewai_cli import git
@@ -161,7 +162,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
self,
env_vars: dict[str, str],
remote_repo_url: str,
) -> dict[str, Any]:
) -> CreateCrewPayload:
"""
Create the payload for crew creation.
@@ -172,6 +173,8 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
Returns:
Dict[str, Any]: The payload for crew creation.
"""
if not self.project_name:
raise ValueError("project_name is required to create a deployment payload")
return {
"deploy": {
"name": self.project_name,

View File

@@ -1,4 +1,4 @@
from functools import lru_cache
from functools import cached_property
import subprocess
@@ -9,7 +9,7 @@ class Repository:
if not self.is_git_installed():
raise ValueError("Git is not installed or not found in your PATH.")
if not self.is_git_repo():
if not self.is_git_repo:
raise ValueError(f"{self.path} is not a Git repository.")
self.fetch()
@@ -40,13 +40,9 @@ class Repository:
encoding="utf-8",
).strip()
@lru_cache(maxsize=None) # noqa: B019
@cached_property # noqa: B019
def is_git_repo(self) -> bool:
"""Check if the current directory is a git repository.
Notes:
- TODO: This method is cached to avoid redundant checks, but using lru_cache on methods can lead to memory leaks
"""
"""Check if the current directory is a git repository."""
try:
subprocess.check_output(
["git", "rev-parse", "--is-inside-work-tree"], # noqa: S607

View File

@@ -1 +1 @@
__version__ = "1.14.5a5"
__version__ = "1.14.5"

View File

@@ -3,36 +3,161 @@
from __future__ import annotations
import os
from typing import Any
from typing import Any, Final, Literal, TypedDict, cast
from urllib.parse import urljoin
import httpx
from typing_extensions import NotRequired
from crewai_core.constants import DEFAULT_CREWAI_ENTERPRISE_URL
from crewai_core.settings import Settings
from crewai_core.version import get_crewai_version
HttpMethod = Literal["GET", "POST", "PATCH", "DELETE"]
class AvailableExport(TypedDict):
name: str
class EnvVarEntry(TypedDict):
name: str
description: str
required: bool
default: str | None
class ToolMetadata(TypedDict):
name: str
module: str
humanized_name: str
description: str
run_params_schema: dict[str, Any]
init_params_schema: dict[str, Any]
env_vars: list[EnvVarEntry]
class ToolsMetadataPayload(TypedDict):
package: str
tools: list[ToolMetadata] | None
class PublishToolPayload(TypedDict):
handle: str
public: bool
version: str
file: str
description: str | None
available_exports: list[AvailableExport] | None
tools_metadata: ToolsMetadataPayload | None
class CrewDeploymentSpec(TypedDict):
name: str
repo_clone_url: str
env: dict[str, str]
class CreateCrewPayload(TypedDict):
deploy: CrewDeploymentSpec
class _WithUserIdentifier(TypedDict):
user_identifier: NotRequired[str]
class LoginPayload(_WithUserIdentifier):
pass
class TraceExecutionContext(TypedDict):
crew_fingerprint: str | None
crew_name: str | None
flow_name: str | None
crewai_version: str
privacy_level: str
class TraceExecutionMetadata(TypedDict):
expected_duration_estimate: int
agent_count: int
task_count: int
flow_method_count: int
execution_started_at: str
class TraceBatchInitPayload(_WithUserIdentifier):
trace_id: str
execution_type: str
execution_context: TraceExecutionContext
execution_metadata: TraceExecutionMetadata
ephemeral_trace_id: NotRequired[str]
class TraceBatchMetadata(TypedDict):
events_count: int
batch_sequence: int
is_final_batch: bool
class TraceEventsPayload(TypedDict):
events: list[dict[str, Any]]
batch_metadata: TraceBatchMetadata
class TraceFinalizePayload(TypedDict):
status: Literal["completed"]
duration_ms: float | None
final_event_count: int
class TraceFailedPayload(TypedDict):
status: Literal["failed"]
failure_reason: str
Headers = TypedDict(
"Headers",
{
"Content-Type": str,
"User-Agent": str,
"X-Crewai-Version": str,
"Authorization": NotRequired[str],
"X-Crewai-Organization-Id": NotRequired[str],
},
)
class RequestKwargs(TypedDict):
headers: dict[str, str]
json: NotRequired[Any]
params: NotRequired[dict[str, str]]
timeout: NotRequired[float]
class PlusAPI:
"""Client for working with the CrewAI+ API."""
TOOLS_RESOURCE = "/crewai_plus/api/v1/tools"
ORGANIZATIONS_RESOURCE = "/crewai_plus/api/v1/me/organizations"
CREWS_RESOURCE = "/crewai_plus/api/v1/crews"
AGENTS_RESOURCE = "/crewai_plus/api/v1/agents"
TRACING_RESOURCE = "/crewai_plus/api/v1/tracing"
EPHEMERAL_TRACING_RESOURCE = "/crewai_plus/api/v1/tracing/ephemeral"
INTEGRATIONS_RESOURCE = "/crewai_plus/api/v1/integrations"
TOOLS_RESOURCE: Final = "/crewai_plus/api/v1/tools"
ORGANIZATIONS_RESOURCE: Final = "/crewai_plus/api/v1/me/organizations"
CREWS_RESOURCE: Final = "/crewai_plus/api/v1/crews"
AGENTS_RESOURCE: Final = "/crewai_plus/api/v1/agents"
TRACING_RESOURCE: Final = "/crewai_plus/api/v1/tracing"
EPHEMERAL_TRACING_RESOURCE: Final = "/crewai_plus/api/v1/tracing/ephemeral"
INTEGRATIONS_RESOURCE: Final = "/crewai_plus/api/v1/integrations"
def __init__(self, api_key: str | None = None) -> None:
version = get_crewai_version()
self.api_key = api_key
self.headers = {
self.headers: Headers = {
"Content-Type": "application/json",
"User-Agent": f"CrewAI-CLI/{get_crewai_version()}",
"X-Crewai-Version": get_crewai_version(),
"User-Agent": f"CrewAI-CLI/{version}",
"X-Crewai-Version": version,
}
if api_key:
self.headers["Authorization"] = f"Bearer {api_key}"
settings = Settings()
if settings.org_uuid:
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid
@@ -44,17 +169,30 @@ class PlusAPI:
)
def _make_request(
self, method: str, endpoint: str, **kwargs: Any
self,
method: HttpMethod,
endpoint: str,
*,
json: Any = None,
params: dict[str, str] | None = None,
timeout: float | None = None,
verify: bool = True,
) -> httpx.Response:
url = urljoin(self.base_url, endpoint)
verify = kwargs.pop("verify", True)
request_kwargs: RequestKwargs = {"headers": cast(dict[str, str], self.headers)}
if json is not None:
request_kwargs["json"] = json
if params is not None:
request_kwargs["params"] = params
if timeout is not None:
request_kwargs["timeout"] = timeout
with httpx.Client(trust_env=False, verify=verify) as client:
return client.request(method, url, headers=self.headers, **kwargs)
return client.request(method, url, **request_kwargs)
def login_to_tool_repository(
self, user_identifier: str | None = None
) -> httpx.Response:
payload = {}
payload: LoginPayload = {}
if user_identifier:
payload["user_identifier"] = user_identifier
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login", json=payload)
@@ -65,7 +203,7 @@ class PlusAPI:
async def get_agent(self, handle: str) -> httpx.Response:
url = urljoin(self.base_url, f"{self.AGENTS_RESOURCE}/{handle}")
async with httpx.AsyncClient() as client:
return await client.get(url, headers=self.headers)
return await client.get(url, headers=cast(dict[str, str], self.headers))
def publish_tool(
self,
@@ -74,10 +212,10 @@ class PlusAPI:
version: str,
description: str | None,
encoded_file: str,
available_exports: list[dict[str, Any]] | None = None,
tools_metadata: list[dict[str, Any]] | None = None,
available_exports: list[AvailableExport] | None = None,
tools_metadata: list[ToolMetadata] | None = None,
) -> httpx.Response:
params = {
params: PublishToolPayload = {
"handle": handle,
"public": is_public,
"version": version,
@@ -129,13 +267,13 @@ class PlusAPI:
def list_crews(self) -> httpx.Response:
return self._make_request("GET", self.CREWS_RESOURCE)
def create_crew(self, payload: dict[str, Any]) -> httpx.Response:
def create_crew(self, payload: CreateCrewPayload) -> httpx.Response:
return self._make_request("POST", self.CREWS_RESOURCE, json=payload)
def get_organizations(self) -> httpx.Response:
return self._make_request("GET", self.ORGANIZATIONS_RESOURCE)
def initialize_trace_batch(self, payload: dict[str, Any]) -> httpx.Response:
def initialize_trace_batch(self, payload: TraceBatchInitPayload) -> httpx.Response:
return self._make_request(
"POST",
f"{self.TRACING_RESOURCE}/batches",
@@ -144,7 +282,7 @@ class PlusAPI:
)
def initialize_ephemeral_trace_batch(
self, payload: dict[str, Any]
self, payload: TraceBatchInitPayload
) -> httpx.Response:
return self._make_request(
"POST",
@@ -153,7 +291,7 @@ class PlusAPI:
)
def send_trace_events(
self, trace_batch_id: str, payload: dict[str, Any]
self, trace_batch_id: str, payload: TraceEventsPayload
) -> httpx.Response:
return self._make_request(
"POST",
@@ -163,7 +301,7 @@ class PlusAPI:
)
def send_ephemeral_trace_events(
self, trace_batch_id: str, payload: dict[str, Any]
self, trace_batch_id: str, payload: TraceEventsPayload
) -> httpx.Response:
return self._make_request(
"POST",
@@ -173,7 +311,7 @@ class PlusAPI:
)
def finalize_trace_batch(
self, trace_batch_id: str, payload: dict[str, Any]
self, trace_batch_id: str, payload: TraceFinalizePayload
) -> httpx.Response:
return self._make_request(
"PATCH",
@@ -183,7 +321,7 @@ class PlusAPI:
)
def finalize_ephemeral_trace_batch(
self, trace_batch_id: str, payload: dict[str, Any]
self, trace_batch_id: str, payload: TraceFinalizePayload
) -> httpx.Response:
return self._make_request(
"PATCH",
@@ -195,20 +333,28 @@ class PlusAPI:
def mark_trace_batch_as_failed(
self, trace_batch_id: str, error_message: str
) -> httpx.Response:
payload: TraceFailedPayload = {
"status": "failed",
"failure_reason": error_message,
}
return self._make_request(
"PATCH",
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}",
json={"status": "failed", "failure_reason": error_message},
json=payload,
timeout=30,
)
def mark_ephemeral_trace_batch_as_failed(
self, trace_batch_id: str, error_message: str
) -> httpx.Response:
payload: TraceFailedPayload = {
"status": "failed",
"failure_reason": error_message,
}
return self._make_request(
"PATCH",
f"{self.EPHEMERAL_TRACING_RESOURCE}/batches/{trace_batch_id}",
json={"status": "failed", "failure_reason": error_message},
json=payload,
timeout=30,
)

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.14.5a5"
__version__ = "1.14.5"

View File

@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
dependencies = [
"pytube~=15.0.0",
"requests>=2.33.0,<3",
"crewai==1.14.5a5",
"crewai==1.14.5",
"tiktoken>=0.8.0,<0.13",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",

View File

@@ -330,4 +330,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.14.5a5"
__version__ = "1.14.5"

View File

@@ -8,8 +8,8 @@ authors = [
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai-core==1.14.5a5",
"crewai-cli==1.14.5a5",
"crewai-core==1.14.5",
"crewai-cli==1.14.5",
# Core Dependencies
"pydantic>=2.11.9,<2.13",
"openai>=2.30.0,<3",
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.14.5a5",
"crewai-tools==1.14.5",
]
embeddings = [
"tiktoken>=0.8.0,<0.13"
@@ -105,7 +105,7 @@ a2a = [
"aiocache[redis,memcached]~=0.12.3",
]
file-processing = [
"crewai-files==1.14.5a5",
"crewai-files",
]
qdrant-edge = [
"qdrant-edge-py>=0.6.0",

View File

@@ -48,7 +48,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.14.5a5"
__version__ = "1.14.5"
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
"Memory": ("crewai.memory.unified_memory", "Memory"),

View File

@@ -36,7 +36,6 @@ from typing_extensions import Self, TypeIs
from crewai.agent.planning_config import PlanningConfig
from crewai.agent.utils import (
ahandle_knowledge_retrieval,
append_skill_context,
apply_training_data,
build_task_prompt_with_schema,
format_task_with_context,
@@ -221,7 +220,11 @@ class Agent(BaseAgent):
str | BaseLLM | None,
BeforeValidator(_validate_llm_ref),
PlainSerializer(_serialize_llm_ref, return_type=dict | None, when_used="json"),
] = Field(description="Language model that will run the agent.", default=None)
] = Field(
description="Language model that will run the agent.",
default=None,
deprecated="function_calling_llm is deprecated and will be removed in a future release.",
)
system_template: str | None = Field(
default=None, description="System format for the agent."
)
@@ -549,7 +552,6 @@ class Agent(BaseAgent):
Returns:
The fully prepared task prompt.
"""
task_prompt = append_skill_context(self, task_prompt)
prepare_tools(self, tools, task)
return apply_training_data(self, task_prompt)
@@ -1486,8 +1488,6 @@ class Agent(BaseAgent):
),
)
formatted_messages = append_skill_context(self, formatted_messages)
inputs: dict[str, Any] = {
"input": formatted_messages,
"tool_names": get_tool_names(parsed_tools),

View File

@@ -213,30 +213,6 @@ def _combine_knowledge_context(agent: Agent) -> str:
return agent_ctx + separator + crew_ctx
def append_skill_context(agent: Agent, task_prompt: str) -> str:
"""Append activated skill context sections to the task prompt.
Args:
agent: The agent with optional skills.
task_prompt: The current task prompt.
Returns:
The task prompt with skill context appended.
"""
if not agent.skills:
return task_prompt
from crewai.skills.loader import format_skill_context
from crewai.skills.models import Skill
skill_sections = [
format_skill_context(s) for s in agent.skills if isinstance(s, Skill)
]
if skill_sections:
task_prompt += "\n\n" + "\n\n".join(skill_sections)
return task_prompt
def apply_training_data(agent: Agent, task_prompt: str) -> str:
"""Apply training data to the task prompt.

View File

@@ -51,7 +51,10 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
_graph: Any = PrivateAttr(default=None)
_memory: Any = PrivateAttr(default=None)
_max_iterations: int = PrivateAttr(default=10)
function_calling_llm: Any = Field(default=None)
function_calling_llm: Any = Field(
default=None,
deprecated="function_calling_llm is deprecated and will be removed in a future release.",
)
step_callback: SerializableCallable | None = Field(default=None)
model: str = Field(default="gpt-4o")

View File

@@ -60,7 +60,10 @@ class OpenAIAgentAdapter(BaseAgentAdapter):
_openai_agent: OpenAIAgentProtocol = PrivateAttr()
_logger: Logger = PrivateAttr(default_factory=Logger)
_active_thread: str | None = PrivateAttr(default=None)
function_calling_llm: Any = Field(default=None)
function_calling_llm: Any = Field(
default=None,
deprecated="function_calling_llm is deprecated and will be removed in a future release.",
)
step_callback: Any = Field(default=None)
_tool_adapter: OpenAIAgentToolAdapter = PrivateAttr()
_converter_adapter: OpenAIConverterAdapter = PrivateAttr()

View File

@@ -174,6 +174,8 @@ class CrewAgentExecutor(BaseAgentExecutor):
if provider.setup_messages(cast(ExecutorContext, cast(object, self))):
return
from crewai.llms.cache import mark_cache_breakpoint
if self.prompt is not None and "system" in self.prompt:
system_prompt = self._format_prompt(
cast(str, self.prompt.get("system", "")), inputs
@@ -181,11 +183,22 @@ class CrewAgentExecutor(BaseAgentExecutor):
user_prompt = self._format_prompt(
cast(str, self.prompt.get("user", "")), inputs
)
self.messages.append(format_message_for_llm(system_prompt, role="system"))
self.messages.append(format_message_for_llm(user_prompt))
# Cache breakpoints: end-of-system caches the per-agent stable
# prefix; end-of-user caches the per-task stable prefix across
# ReAct-loop iterations.
self.messages.append(
mark_cache_breakpoint(
format_message_for_llm(system_prompt, role="system")
)
)
self.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
elif self.prompt is not None:
user_prompt = self._format_prompt(self.prompt.get("prompt", ""), inputs)
self.messages.append(format_message_for_llm(user_prompt))
self.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
provider.post_setup_messages(cast(ExecutorContext, cast(object, self)))

View File

@@ -251,7 +251,11 @@ class Crew(FlowTrackable, BaseModel):
str | LLM | None,
BeforeValidator(_validate_llm_ref),
PlainSerializer(_serialize_llm_ref, return_type=dict | None, when_used="json"),
] = Field(description="Language model that will run the agent.", default=None)
] = Field(
description="Language model that will run the agent.",
default=None,
deprecated="function_calling_llm is deprecated and will be removed in a future release.",
)
config: Json[dict[str, Any]] | dict[str, Any] | None = Field(default=None)
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
share_crew: bool | None = Field(default=False)

View File

@@ -6,6 +6,14 @@ import time
from typing import Any
import uuid
from crewai_core.plus_api import (
TraceBatchInitPayload,
TraceBatchMetadata,
TraceEventsPayload,
TraceExecutionContext,
TraceExecutionMetadata,
TraceFinalizePayload,
)
from crewai_core.settings import Settings
from rich.console import Console
from rich.panel import Panel
@@ -123,25 +131,27 @@ class TraceBatchManager:
return None
try:
payload = {
execution_context: TraceExecutionContext = {
"crew_fingerprint": execution_metadata.get("crew_fingerprint"),
"crew_name": execution_metadata.get("crew_name", None),
"flow_name": execution_metadata.get("flow_name", None),
"crewai_version": self.current_batch.version,
"privacy_level": user_context.get("privacy_level", "standard"),
}
execution_metadata_payload: TraceExecutionMetadata = {
"expected_duration_estimate": execution_metadata.get(
"expected_duration_estimate", 300
),
"agent_count": execution_metadata.get("agent_count", 0),
"task_count": execution_metadata.get("task_count", 0),
"flow_method_count": execution_metadata.get("flow_method_count", 0),
"execution_started_at": datetime.now(timezone.utc).isoformat(),
}
payload: TraceBatchInitPayload = {
"trace_id": self.current_batch.batch_id,
"execution_type": execution_metadata.get("execution_type", "crew"),
"execution_context": {
"crew_fingerprint": execution_metadata.get("crew_fingerprint"),
"crew_name": execution_metadata.get("crew_name", None),
"flow_name": execution_metadata.get("flow_name", None),
"crewai_version": self.current_batch.version,
"privacy_level": user_context.get("privacy_level", "standard"),
},
"execution_metadata": {
"expected_duration_estimate": execution_metadata.get(
"expected_duration_estimate", 300
),
"agent_count": execution_metadata.get("agent_count", 0),
"task_count": execution_metadata.get("task_count", 0),
"flow_method_count": execution_metadata.get("flow_method_count", 0),
"execution_started_at": datetime.now(timezone.utc).isoformat(),
},
"execution_context": execution_context,
"execution_metadata": execution_metadata_payload,
}
if use_ephemeral:
payload["ephemeral_trace_id"] = self.current_batch.batch_id
@@ -264,13 +274,14 @@ class TraceBatchManager:
if not self.plus_api or not self.trace_batch_id or not self.event_buffer:
return 500
try:
payload = {
batch_metadata: TraceBatchMetadata = {
"events_count": len(self.event_buffer),
"batch_sequence": 1,
"is_final_batch": False,
}
payload: TraceEventsPayload = {
"events": [event.to_dict() for event in self.event_buffer],
"batch_metadata": {
"events_count": len(self.event_buffer),
"batch_sequence": 1,
"is_final_batch": False,
},
"batch_metadata": batch_metadata,
}
response = (
@@ -364,7 +375,7 @@ class TraceBatchManager:
return
try:
payload = {
payload: TraceFinalizePayload = {
"status": "completed",
"duration_ms": self.calculate_duration("execution"),
"final_event_count": events_count,

View File

@@ -2586,16 +2586,26 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self._kickoff_input = inputs.get("input", "")
if "system" in self.prompt:
from crewai.llms.cache import mark_cache_breakpoint
prompt = cast("SystemPromptResult", self.prompt)
system_prompt = self._format_prompt(prompt["system"], inputs)
user_prompt = self._format_prompt(prompt["user"], inputs)
self.state.messages.append(
format_message_for_llm(system_prompt, role="system")
mark_cache_breakpoint(
format_message_for_llm(system_prompt, role="system")
)
)
self.state.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
self.state.messages.append(format_message_for_llm(user_prompt))
else:
from crewai.llms.cache import mark_cache_breakpoint
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
self.state.messages.append(format_message_for_llm(user_prompt))
self.state.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
self._inject_files_from_inputs(inputs)
@@ -2677,16 +2687,26 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self._kickoff_input = inputs.get("input", "")
if "system" in self.prompt:
from crewai.llms.cache import mark_cache_breakpoint
prompt = cast("SystemPromptResult", self.prompt)
system_prompt = self._format_prompt(prompt["system"], inputs)
user_prompt = self._format_prompt(prompt["user"], inputs)
self.state.messages.append(
format_message_for_llm(system_prompt, role="system")
mark_cache_breakpoint(
format_message_for_llm(system_prompt, role="system")
)
)
self.state.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
self.state.messages.append(format_message_for_llm(user_prompt))
else:
from crewai.llms.cache import mark_cache_breakpoint
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
self.state.messages.append(format_message_for_llm(user_prompt))
self.state.messages.append(
mark_cache_breakpoint(format_message_for_llm(user_prompt))
)
self._inject_files_from_inputs(inputs)

View File

@@ -2633,6 +2633,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
the event_id of the MethodExecutionFinishedEvent, or None if events
are suppressed.
"""
logger.info("Executing flow method: %s", method_name)
try:
dumped_params = {f"_{i}": arg for i, arg in enumerate(args)} | (
kwargs or {}

View File

@@ -940,6 +940,21 @@ class LLM(BaseLLM):
self._track_token_usage_internal(usage_info)
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
if accumulated_tool_args and not available_functions:
tool_calls_list: list[ChatCompletionDeltaToolCall] = [
ChatCompletionDeltaToolCall(
index=idx,
function=Function(
name=tool_arg.function.name,
arguments=tool_arg.function.arguments,
),
)
for idx, tool_arg in sorted(accumulated_tool_args.items())
if tool_arg.function.name
]
if tool_calls_list:
return tool_calls_list
if not tool_calls or not available_functions:
if response_model and self.is_litellm:
instructor_instance = InternalInstructor(
@@ -1535,8 +1550,7 @@ class LLM(BaseLLM):
if usage_info:
self._track_token_usage_internal(usage_info)
if accumulated_tool_args and available_functions:
# Convert accumulated tool args to ChatCompletionDeltaToolCall objects
if accumulated_tool_args:
tool_calls_list: list[ChatCompletionDeltaToolCall] = [
ChatCompletionDeltaToolCall(
index=idx,
@@ -1545,21 +1559,24 @@ class LLM(BaseLLM):
arguments=tool_arg.function.arguments,
),
)
for idx, tool_arg in accumulated_tool_args.items()
for idx, tool_arg in sorted(accumulated_tool_args.items())
if tool_arg.function.name
]
if tool_calls_list:
result = self._handle_streaming_tool_calls(
tool_calls=tool_calls_list,
accumulated_tool_args=accumulated_tool_args,
available_functions=available_functions,
from_task=from_task,
from_agent=from_agent,
response_id=response_id,
)
if result is not None:
return result
if available_functions:
result = self._handle_streaming_tool_calls(
tool_calls=tool_calls_list,
accumulated_tool_args=accumulated_tool_args,
available_functions=available_functions,
from_task=from_task,
from_agent=from_agent,
response_id=response_id,
)
if result is not None:
return result
else:
return tool_calls_list
usage_dict = self._usage_to_dict(usage_info)
self._handle_emit_call_events(

View File

@@ -14,7 +14,7 @@ from datetime import datetime
import json
import logging
import re
from typing import TYPE_CHECKING, Any, Final, Literal
from typing import TYPE_CHECKING, Any, Final, Literal, cast
import uuid
from pydantic import (
@@ -703,10 +703,19 @@ class BaseLLM(BaseModel, ABC):
Raises:
ValueError: If message format is invalid
"""
from crewai.llms.cache import CACHE_BREAKPOINT_KEY
from crewai.utilities.types import LLMMessage as _LLMMessage
if isinstance(messages, str):
return [{"role": "user", "content": messages}]
# Validate message format
# Validate then copy each message, dropping the cache-breakpoint
# flag in the copy only. The caller (e.g. CrewAgentExecutor,
# experimental.AgentExecutor) reuses its messages buffer across
# many LLM calls in the tool-use loop; mutating their dicts
# in place would erase the markers after the first call and
# break prompt caching for every subsequent iteration.
cleaned: list[LLMMessage] = []
for i, msg in enumerate(messages):
if not isinstance(msg, dict):
raise ValueError(f"Message at index {i} must be a dictionary")
@@ -714,8 +723,12 @@ class BaseLLM(BaseModel, ABC):
raise ValueError(
f"Message at index {i} must have 'role' and 'content' keys"
)
copy: dict[str, Any] = {
k: v for k, v in msg.items() if k != CACHE_BREAKPOINT_KEY
}
cleaned.append(cast(_LLMMessage, copy))
return self._process_message_files(messages)
return self._process_message_files(cleaned)
def _process_message_files(self, messages: list[LLMMessage]) -> list[LLMMessage]:
"""Process files attached to messages and format for the provider.

View File

@@ -0,0 +1,37 @@
"""Provider-agnostic prompt-cache breakpoint marker.
Application code (prompt builders, agent executors) marks messages where a
stable prefix ends. Provider adapters then translate the marker into the
cache directive their API expects, or strip it for providers that cache
implicitly (OpenAI, Gemini) or do not cache at all.
Usage:
from crewai.llms.cache import mark_cache_breakpoint
messages = [
mark_cache_breakpoint({"role": "system", "content": stable_system}),
mark_cache_breakpoint({"role": "user", "content": stable_user_prefix}),
{"role": "user", "content": volatile_query},
]
"""
from __future__ import annotations
from typing import Any
CACHE_BREAKPOINT_KEY = "cache_breakpoint"
def mark_cache_breakpoint(message: dict[str, Any]) -> dict[str, Any]:
"""Return ``message`` with the cache-breakpoint flag set.
Returns a new dict so callers can safely pass literal dicts.
"""
return {**message, CACHE_BREAKPOINT_KEY: True}
def strip_cache_breakpoint(message: dict[str, Any]) -> None:
"""Remove the breakpoint flag from a message in place."""
message.pop(CACHE_BREAKPOINT_KEY, None)

View File

@@ -425,7 +425,7 @@ class AnthropicCompletion(BaseLLM):
def _prepare_completion_params(
self,
messages: list[LLMMessage],
system_message: str | None = None,
system_message: str | list[dict[str, Any]] | None = None,
tools: list[dict[str, Any]] | None = None,
available_functions: dict[str, Any] | None = None,
) -> dict[str, Any]:
@@ -665,7 +665,7 @@ class AnthropicCompletion(BaseLLM):
def _format_messages_for_anthropic(
self, messages: str | list[LLMMessage]
) -> tuple[list[LLMMessage], str | None]:
) -> tuple[list[LLMMessage], str | list[dict[str, Any]] | None]:
"""Format messages for Anthropic API.
Anthropic has specific requirements:
@@ -679,8 +679,51 @@ class AnthropicCompletion(BaseLLM):
messages: Input messages
Returns:
Tuple of (formatted_messages, system_message)
Tuple of (formatted_messages, system_message). `system_message` is
a list of content blocks (with cache_control stamped) when any
system message in the input carried a cache_breakpoint flag;
otherwise a plain string for backwards compatibility.
"""
from crewai.llms.cache import CACHE_BREAKPOINT_KEY
# Read cache_breakpoint flags from raw input BEFORE super strips them.
# We track the CONTENT of marked user/assistant messages so we can
# locate the corresponding block in formatted_messages — Anthropic
# rewrites tool results into user messages, so positional indices
# do not survive the conversion. We must stamp the original stable
# message (typically the initial task prompt), not whatever happens
# to be the trailing user-role block after tool_result expansion.
cache_system = False
cache_match_contents: list[str] = []
if not isinstance(messages, str):
for m in messages:
if not (isinstance(m, dict) and m.get(CACHE_BREAKPOINT_KEY)):
continue
role = m.get("role")
if role == "system":
cache_system = True
continue
if role != "user":
# Only user messages survive Anthropic's role-coalescing
# in a stable, addressable position. Markers on assistant
# or tool messages have no reliable stamp target after
# tool_result expansion, so we ignore them.
continue
raw_content = m.get("content")
if isinstance(raw_content, str) and raw_content:
cache_match_contents.append(raw_content)
continue
if isinstance(raw_content, list):
# Pull text from a single-text-block list so callers that
# pre-format content blocks still match cleanly.
text_blocks = [
b.get("text")
for b in raw_content
if isinstance(b, dict) and b.get("type") == "text"
]
if len(text_blocks) == 1 and isinstance(text_blocks[0], str):
cache_match_contents.append(text_blocks[0])
# Use base class formatting first
base_formatted = super()._format_messages(messages)
@@ -788,7 +831,62 @@ class AnthropicCompletion(BaseLLM):
# If first message is not from user, insert a user message at the beginning
formatted_messages.insert(0, {"role": "user", "content": "Hello"})
return formatted_messages, system_message
# Stamp cache_control on the message(s) whose original content was
# marked. We scan formatted_messages in order and stamp the first
# match per marked content — Anthropic permits up to 4 cache
# breakpoints per request, which is more than enough for our usage.
# Matching by content (rather than position) handles the ReAct
# case where tool_result blocks get expanded into trailing user
# messages: the stable initial-task prompt still maps cleanly.
for needle in cache_match_contents:
for fm in formatted_messages:
if fm.get("role") != "user":
continue
content = fm.get("content")
if isinstance(content, str) and content == needle:
self._stamp_cache_control_on_message(fm)
break
if isinstance(content, list):
fm_texts: list[str] = [
b.get("text", "")
for b in content
if isinstance(b, dict) and b.get("type") == "text"
]
if len(fm_texts) == 1 and fm_texts[0] == needle:
self._stamp_cache_control_on_message(fm)
break
# Convert system to content-block form when caching is requested.
system_payload: str | list[dict[str, Any]] | None = system_message
if system_message and cache_system:
system_payload = [
{
"type": "text",
"text": system_message,
"cache_control": {"type": "ephemeral"},
}
]
return formatted_messages, system_payload
@staticmethod
def _stamp_cache_control_on_message(message: LLMMessage) -> None:
"""Stamp cache_control on the last content block of an Anthropic message."""
msg = cast(dict[str, Any], message)
content = msg.get("content")
if isinstance(content, str):
msg["content"] = [
{
"type": "text",
"text": content,
"cache_control": {"type": "ephemeral"},
}
]
return
if isinstance(content, list) and content:
last = content[-1]
if isinstance(last, dict):
last["cache_control"] = {"type": "ephemeral"}
def _handle_completion(
self,

View File

@@ -161,6 +161,9 @@ def format_skill_context(skill: Skill) -> str:
At METADATA level: returns name and description only.
At INSTRUCTIONS level or above: returns full SKILL.md body.
Output is wrapped in <skill name="..."> XML tags so the block can serve
as a stable cache anchor when injected into the system prompt.
Args:
skill: The skill to format.
@@ -169,7 +172,7 @@ def format_skill_context(skill: Skill) -> str:
"""
if skill.disclosure_level >= INSTRUCTIONS and skill.instructions:
parts = [
f"## Skill: {skill.name}",
f'<skill name="{skill.name}">',
skill.description,
"",
skill.instructions,
@@ -180,5 +183,6 @@ def format_skill_context(skill: Skill) -> str:
for dir_name, files in sorted(skill.resource_files.items()):
if files:
parts.append(f"- **{dir_name}/**: {', '.join(files)}")
parts.append("</skill>")
return "\n".join(parts)
return f"## Skill: {skill.name}\n{skill.description}"
return f'<skill name="{skill.name}">\n{skill.description}\n</skill>'

View File

@@ -13,6 +13,7 @@ import sys
import types
from typing import Any, cast, get_type_hints
from crewai_core.plus_api import AvailableExport, EnvVarEntry, ToolMetadata
from crewai_core.project import (
get_project_description as get_project_description,
get_project_name as get_project_name,
@@ -279,7 +280,7 @@ def is_valid_tool(obj: Any) -> bool:
return isinstance(obj, Tool)
def extract_available_exports(dir_path: str = "src") -> list[dict[str, Any]]:
def extract_available_exports(dir_path: str = "src") -> list[AvailableExport]:
"""Extract available tool classes from the project's __init__.py files.
Only includes classes that inherit from BaseTool or functions decorated with @tool.
@@ -338,7 +339,7 @@ def _load_module_from_file(
sys.modules.pop(module_name, None)
def _load_tools_from_init(init_file: Path) -> list[dict[str, Any]]:
def _load_tools_from_init(init_file: Path) -> list[AvailableExport]:
"""Load and validate tools from a given __init__.py file."""
try:
with _load_module_from_file(init_file) as module:
@@ -392,7 +393,7 @@ def _print_no_tools_warning() -> None:
)
def extract_tools_metadata(dir_path: str = "src") -> list[dict[str, Any]]:
def extract_tools_metadata(dir_path: str = "src") -> list[ToolMetadata]:
"""
Extract rich metadata from tool classes in the project.
@@ -404,7 +405,7 @@ def extract_tools_metadata(dir_path: str = "src") -> list[dict[str, Any]]:
- init_params_schema: JSON Schema for __init__ params (filtered)
- env_vars: List of environment variable dicts
"""
tools_metadata: list[dict[str, Any]] = []
tools_metadata: list[ToolMetadata] = []
for init_file in Path(dir_path).glob("**/__init__.py"):
tools = _extract_tool_metadata_from_init(init_file)
@@ -413,7 +414,7 @@ def extract_tools_metadata(dir_path: str = "src") -> list[dict[str, Any]]:
return tools_metadata
def _extract_tool_metadata_from_init(init_file: Path) -> list[dict[str, Any]]:
def _extract_tool_metadata_from_init(init_file: Path) -> list[ToolMetadata]:
"""
Load module from init file and extract metadata from valid tool classes.
"""
@@ -428,7 +429,7 @@ def _extract_tool_metadata_from_init(init_file: Path) -> list[dict[str, Any]]:
if not exported_names:
return []
tools_metadata = []
tools_metadata: list[ToolMetadata] = []
for name in exported_names:
obj = getattr(module, name, None)
if obj is None or not (
@@ -446,7 +447,7 @@ def _extract_tool_metadata_from_init(init_file: Path) -> list[dict[str, Any]]:
return []
def _extract_single_tool_metadata(tool_class: type) -> dict[str, Any] | None:
def _extract_single_tool_metadata(tool_class: type) -> ToolMetadata | None:
"""
Extract metadata from a single tool class.
"""
@@ -470,19 +471,17 @@ def _extract_single_tool_metadata(tool_class: type) -> dict[str, Any] | None:
except (TypeError, ValueError):
module = tool_class.__module__
return {
"name": tool_class.__name__,
"module": module,
"humanized_name": _extract_field_default(
fields.get("name"), fallback=tool_class.__name__
return ToolMetadata(
name=tool_class.__name__,
module=module,
humanized_name=str(
_extract_field_default(fields.get("name"), fallback=tool_class.__name__)
),
"description": str(
_extract_field_default(fields.get("description"))
).strip(),
"run_params_schema": _extract_run_params_schema(fields.get("args_schema")),
"init_params_schema": _extract_init_params_schema(tool_class),
"env_vars": _extract_env_vars(fields.get("env_vars")),
}
description=str(_extract_field_default(fields.get("description"))).strip(),
run_params_schema=_extract_run_params_schema(fields.get("args_schema")),
init_params_schema=_extract_init_params_schema(tool_class),
env_vars=_extract_env_vars(fields.get("env_vars")),
)
except Exception:
return None
@@ -597,7 +596,7 @@ def _extract_init_params_schema(tool_class: type) -> dict[str, Any]:
return {}
def _extract_env_vars(env_vars_field: dict[str, Any] | None) -> list[dict[str, Any]]:
def _extract_env_vars(env_vars_field: dict[str, Any] | None) -> list[EnvVarEntry]:
"""
Extract environment variable definitions from env_vars field.
"""

View File

@@ -86,7 +86,7 @@ class Prompts(BaseModel):
slices.append("tools")
else:
slices.append("no_tools")
system: str = self._build_prompt(slices)
system: str = self._build_prompt(slices) + self._build_skill_block()
# Determine which task slice to use:
task_slice: COMPONENTS
@@ -106,7 +106,7 @@ class Prompts(BaseModel):
return SystemPromptResult(
system=system,
user=self._build_prompt([task_slice]),
prompt=self._build_prompt(slices),
prompt=self._build_prompt(slices) + self._build_skill_block(),
)
return StandardPromptResult(
prompt=self._build_prompt(
@@ -115,8 +115,27 @@ class Prompts(BaseModel):
self.prompt_template,
self.response_template,
)
+ self._build_skill_block()
)
def _build_skill_block(self) -> str:
"""Render the agent's activated skills as a stable XML block.
Skills are agent-scoped (do not change per task), so they live in the
system prompt where prompt-cache prefixes can survive across calls.
"""
skills = getattr(self.agent, "skills", None)
if not skills:
return ""
from crewai.skills.loader import format_skill_context
from crewai.skills.models import Skill
sections = [format_skill_context(s) for s in skills if isinstance(s, Skill)]
if not sections:
return ""
return "\n\n<skills>\n" + "\n\n".join(sections) + "\n</skills>"
def _build_prompt(
self,
components: list[COMPONENTS],

View File

@@ -0,0 +1,196 @@
"""Regression tests for the provider-agnostic prompt-cache breakpoint flag."""
from __future__ import annotations
from crewai.llms.cache import (
CACHE_BREAKPOINT_KEY,
mark_cache_breakpoint,
strip_cache_breakpoint,
)
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
from crewai.llms.providers.openai.completion import OpenAICompletion
class TestCacheMarkerHelpers:
def test_mark_returns_new_dict(self) -> None:
original = {"role": "user", "content": "hi"}
marked = mark_cache_breakpoint(original)
assert marked[CACHE_BREAKPOINT_KEY] is True
# Marker must NOT bleed back into the caller's dict — callers may
# pass literal dicts and reuse them across calls.
assert CACHE_BREAKPOINT_KEY not in original
def test_strip_is_idempotent(self) -> None:
msg = {"role": "user", "content": "hi", CACHE_BREAKPOINT_KEY: True}
strip_cache_breakpoint(msg)
assert CACHE_BREAKPOINT_KEY not in msg
strip_cache_breakpoint(msg)
assert CACHE_BREAKPOINT_KEY not in msg
class TestBaseFormatDoesNotMutate:
"""The strip-on-format pass must not erase markers from the caller's
messages list — executors reuse a single list across many LLM calls,
and mutating it would defeat caching on every iteration after the first.
"""
def test_repeated_format_preserves_markers(self) -> None:
llm = OpenAICompletion(model="gpt-4o-mini")
messages = [
mark_cache_breakpoint({"role": "system", "content": "stable system"}),
mark_cache_breakpoint({"role": "user", "content": "stable user"}),
]
# First call: provider strips markers from the returned (copied) list
first = llm._format_messages(messages)
assert all(CACHE_BREAKPOINT_KEY not in m for m in first)
# Original list must STILL carry the markers
assert messages[0][CACHE_BREAKPOINT_KEY] is True
assert messages[1][CACHE_BREAKPOINT_KEY] is True
# Second call from the same list still sees the markers
second = llm._format_messages(messages)
assert all(CACHE_BREAKPOINT_KEY not in m for m in second)
assert messages[0][CACHE_BREAKPOINT_KEY] is True
assert messages[1][CACHE_BREAKPOINT_KEY] is True
class TestAnthropicCacheStamping:
def test_stamps_system_with_cache_control(self) -> None:
llm = AnthropicCompletion(model="claude-sonnet-4-5")
messages = [
mark_cache_breakpoint({"role": "system", "content": "you are helpful"}),
mark_cache_breakpoint({"role": "user", "content": "ping"}),
]
formatted, system = llm._format_messages_for_anthropic(messages)
assert isinstance(system, list)
assert system[0]["cache_control"] == {"type": "ephemeral"}
assert system[0]["text"] == "you are helpful"
# First user block carries cache_control too
last_block = formatted[0]["content"][-1]
assert last_block["cache_control"] == {"type": "ephemeral"}
def test_stamps_stable_user_not_tool_result(self) -> None:
"""Within a ReAct loop, tool results are flattened into a trailing
user message. We must NOT stamp that volatile trailing block — we
must stamp the original stable user prompt instead.
"""
llm = AnthropicCompletion(model="claude-sonnet-4-5")
messages = [
mark_cache_breakpoint({"role": "system", "content": "you are helpful"}),
mark_cache_breakpoint({"role": "user", "content": "stable task prompt"}),
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "tc_1",
"function": {"name": "ping", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "tc_1", "content": "volatile tool result"},
]
formatted, _system = llm._format_messages_for_anthropic(messages)
# Find the message that holds the stable prompt
stable = next(
fm
for fm in formatted
if fm["role"] == "user"
and isinstance(fm["content"], list)
and any(
isinstance(b, dict)
and b.get("type") == "text"
and b.get("text") == "stable task prompt"
for b in fm["content"]
)
)
text_block = next(
b for b in stable["content"] if isinstance(b, dict) and b.get("type") == "text"
)
assert text_block.get("cache_control") == {"type": "ephemeral"}
# The tool_result-bearing user message must NOT be stamped
tool_carrier = next(
fm
for fm in formatted
if fm["role"] == "user"
and isinstance(fm["content"], list)
and any(
isinstance(b, dict) and b.get("type") == "tool_result"
for b in fm["content"]
)
)
for block in tool_carrier["content"]:
assert "cache_control" not in block
def test_assistant_marker_is_ignored(self) -> None:
"""Markers on assistant messages have no stable stamp target after
Anthropic's role coalescing, so they should be silently ignored
rather than collected and then dropped on a mismatch.
"""
llm = AnthropicCompletion(model="claude-sonnet-4-5")
messages = [
mark_cache_breakpoint({"role": "system", "content": "you are helpful"}),
mark_cache_breakpoint(
{"role": "assistant", "content": "I will help you out."}
),
{"role": "user", "content": "ping"},
]
formatted, system = llm._format_messages_for_anthropic(messages)
# System still cached
assert isinstance(system, list)
# No user message was marked → no user message should carry cache_control
for fm in formatted:
if fm.get("role") != "user":
continue
content = fm.get("content")
if isinstance(content, list):
for block in content:
if isinstance(block, dict):
assert "cache_control" not in block
def test_list_content_user_marker_matches(self) -> None:
"""A pre-formatted user message with a single text block should still
match against the post-format user message.
"""
llm = AnthropicCompletion(model="claude-sonnet-4-5")
messages = [
mark_cache_breakpoint(
{
"role": "user",
"content": [{"type": "text", "text": "stable list prompt"}],
}
),
]
formatted, _system = llm._format_messages_for_anthropic(messages)
user_msg = next(fm for fm in formatted if fm["role"] == "user")
content = user_msg["content"]
assert isinstance(content, list)
text_block = next(b for b in content if isinstance(b, dict) and b.get("type") == "text")
assert text_block.get("cache_control") == {"type": "ephemeral"}
def test_unmarked_messages_get_no_cache_control(self) -> None:
llm = AnthropicCompletion(model="claude-sonnet-4-5")
messages = [
{"role": "system", "content": "no caching here"},
{"role": "user", "content": "no caching here either"},
]
formatted, system = llm._format_messages_for_anthropic(messages)
# No marker → system stays a plain string (no content-block conversion)
assert isinstance(system, str)
# No marker → no cache_control anywhere in formatted messages
for fm in formatted:
content = fm.get("content")
if isinstance(content, list):
for block in content:
assert "cache_control" not in block
class TestNonAnthropicStripsMarker:
def test_openai_format_strips_marker_from_wire_payload(self) -> None:
llm = OpenAICompletion(model="gpt-4o-mini")
messages = [
mark_cache_breakpoint({"role": "system", "content": "stable"}),
mark_cache_breakpoint({"role": "user", "content": "hi"}),
]
formatted = llm._format_messages(messages)
for m in formatted:
assert CACHE_BREAKPOINT_KEY not in m

View File

@@ -5,9 +5,9 @@ from pathlib import Path
import pytest
from crewai import Agent
from crewai.agent.utils import append_skill_context
from crewai.skills.loader import activate_skill, discover_skills, format_skill_context
from crewai.skills.models import INSTRUCTIONS, METADATA
from crewai.utilities.prompts import Prompts
def _create_skill_dir(parent: Path, name: str, body: str = "Body.") -> Path:
@@ -34,7 +34,7 @@ class TestSkillDiscoveryAndActivation:
assert activated.instructions == "Use this skill."
context = format_skill_context(activated)
assert "## Skill: my-skill" in context
assert '<skill name="my-skill">' in context
assert "Use this skill." in context
def test_filter_by_skill_names(self, tmp_path: Path) -> None:
@@ -94,7 +94,9 @@ class TestSkillDiscoveryAndActivation:
assert agent.skills[0].disclosure_level == METADATA
assert agent.skills[0].instructions is None
prompt = append_skill_context(agent, "Plan a 10-day Japan itinerary.")
assert "## Skill: travel" in prompt
assert "Skill travel" in prompt
assert "Use this skill for travel planning." not in prompt
result = Prompts(agent=agent, has_tools=False, use_system_prompt=True).task_execution()
system = getattr(result, "system", "") or result.prompt
assert '<skill name="travel">' in system
assert "Skill travel" in system
# METADATA-level skills must not leak full instructions into the prompt
assert "Use this skill for travel planning." not in system

View File

@@ -105,7 +105,7 @@ class TestFormatSkillContext:
frontmatter=fm, path=tmp_path, disclosure_level=METADATA
)
ctx = format_skill_context(skill)
assert "## Skill: test-skill" in ctx
assert '<skill name="test-skill">' in ctx
assert "A skill" in ctx
def test_instructions_level(self, tmp_path: Path) -> None:
@@ -117,7 +117,7 @@ class TestFormatSkillContext:
instructions="Do these things.",
)
ctx = format_skill_context(skill)
assert "## Skill: test-skill" in ctx
assert '<skill name="test-skill">' in ctx
assert "Do these things." in ctx
def test_no_instructions_at_instructions_level(self, tmp_path: Path) -> None:
@@ -129,7 +129,7 @@ class TestFormatSkillContext:
instructions=None,
)
ctx = format_skill_context(skill)
assert ctx == "## Skill: test-skill\nA skill"
assert ctx == '<skill name="test-skill">\nA skill\n</skill>'
def test_resources_level(self, tmp_path: Path) -> None:
fm = SkillFrontmatter(name="test-skill", description="A skill")

View File

@@ -624,12 +624,15 @@ def test_handle_streaming_tool_calls_no_available_functions(
],
tools=[get_weather_tool_schema],
)
assert response == ""
assert isinstance(response, list)
assert len(response) == 1
assert response[0].function.name == "get_weather"
assert response[0].function.arguments == '{"location":"New York, NY"}'
assert_event_count(
mock_emit=mock_emit,
expected_stream_chunk=9,
expected_completed_llm_call=1,
expected_completed_llm_call=0,
expected_final_chunk_result='{"location":"New York, NY"}',
)

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.14.5a5"
__version__ = "1.14.5"

View File

@@ -3,6 +3,7 @@
from collections.abc import Mapping
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
@@ -355,8 +356,19 @@ def update_pyproject_dependencies(
workspace_packages = _DEFAULT_WORKSPACE_PACKAGES + (extra_packages or [])
current_extra: str | None = None
extra_header = re.compile(r"^\s*([A-Za-z0-9_-]+)\s*=\s*\[")
for i, line in enumerate(lines):
match = extra_header.match(line)
if match:
current_extra = match.group(1)
elif line.strip().startswith("]"):
current_extra = None
for pkg in workspace_packages:
if pkg == "crewai-files" and current_extra == "file-processing":
continue
if f"{pkg}==" in line:
stripped = line.lstrip()
indent = line[: len(line) - len(stripped)]
@@ -732,18 +744,23 @@ def _is_prerelease(version: str) -> bool:
return any(indicator in v for indicator in _PRERELEASE_INDICATORS)
def get_commits_from_last_tag(tag_name: str, version: str) -> tuple[str, str]:
def get_commits_from_last_tag(
tag_name: str, version: str, cwd: Path | None = None
) -> tuple[str, str]:
"""Get commits from the last tag, excluding current version.
Args:
tag_name: Current tag name (e.g., "v1.0.0").
version: Current version (e.g., "1.0.0").
cwd: Directory to run git commands in (defaults to current).
Returns:
Tuple of (commit_range, commits) where commits is newline-separated.
"""
try:
all_tags = run_command(["git", "tag", "--sort=-version:refname"]).split("\n")
all_tags = run_command(
["git", "tag", "--sort=-version:refname"], cwd=cwd
).split("\n")
prev_tags = [t for t in all_tags if t and t != tag_name and t != f"v{version}"]
if not _is_prerelease(version):
@@ -752,22 +769,30 @@ def get_commits_from_last_tag(tag_name: str, version: str) -> tuple[str, str]:
if prev_tags:
last_tag = prev_tags[0]
commit_range = f"{last_tag}..HEAD"
commits = run_command(["git", "log", commit_range, "--pretty=format:%s"])
commits = run_command(
["git", "log", commit_range, "--pretty=format:%s"], cwd=cwd
)
else:
commit_range = "HEAD"
commits = run_command(["git", "log", "--pretty=format:%s"])
commits = run_command(["git", "log", "--pretty=format:%s"], cwd=cwd)
except subprocess.CalledProcessError:
commit_range = "HEAD"
commits = run_command(["git", "log", "--pretty=format:%s"])
commits = run_command(["git", "log", "--pretty=format:%s"], cwd=cwd)
return commit_range, commits
def get_github_contributors(commit_range: str) -> list[str]:
def get_github_contributors(
commit_range: str,
repo: str = "crewAIInc/crewAI",
cwd: Path | None = None,
) -> list[str]:
"""Get GitHub usernames from commit range using GitHub API.
Args:
commit_range: Git commit range (e.g., "abc123..HEAD").
repo: GitHub repo in ``owner/name`` form to resolve commits against.
cwd: Directory to run git commands in (defaults to current).
Returns:
List of GitHub usernames sorted alphabetically.
@@ -779,10 +804,10 @@ def get_github_contributors(commit_range: str) -> list[str]:
gh_token = None
g = Github(login_or_token=gh_token) if gh_token else Github()
github_repo = g.get_repo("crewAIInc/crewAI")
github_repo = g.get_repo(repo)
commit_shas = run_command(
["git", "log", commit_range, "--pretty=format:%H"]
["git", "log", commit_range, "--pretty=format:%H"], cwd=cwd
).split("\n")
contributors = set()
@@ -922,9 +947,26 @@ def _generate_release_notes(
version: str,
tag_name: str,
no_edit: bool,
cwd: Path | None = None,
gh_repo: str = "crewAIInc/crewAI",
openai_client: OpenAI | None = None,
bump_already_done: bool = True,
) -> tuple[str, OpenAI, bool]:
"""Generate, display, and optionally edit release notes.
Args:
version: Version being released.
tag_name: Tag name for the release.
no_edit: Skip the interactive edit prompt.
cwd: Directory to run git commands in (defaults to current).
gh_repo: GitHub repo (``owner/name``) for resolving contributors.
openai_client: Reuse an existing OpenAI client if provided.
bump_already_done: True when the ``feat: bump versions to <version>``
commit for the current release is already in history (the real
release path). False in previews where no bump exists yet — the
most recent bump commit is the *previous* version and must be
used as the range start.
Returns:
Tuple of (release_notes, openai_client, is_prerelease).
"""
@@ -939,7 +981,8 @@ def _generate_release_notes(
"log",
"--grep=^feat: bump versions to",
"--format=%H %s",
]
],
cwd=cwd,
)
bump_entries = [
line for line in prev_bump_output.strip().split("\n") if line.strip()
@@ -947,7 +990,8 @@ def _generate_release_notes(
is_stable = not _is_prerelease(version)
prev_commit = None
for entry in bump_entries[1:]:
scan_entries = bump_entries[1:] if bump_already_done else bump_entries
for entry in scan_entries:
bump_ver = entry.split("feat: bump versions to", 1)[-1].strip()
if is_stable and _is_prerelease(bump_ver):
continue
@@ -957,7 +1001,7 @@ def _generate_release_notes(
if prev_commit:
commit_range = f"{prev_commit}..HEAD"
commits = run_command(
["git", "log", commit_range, "--pretty=format:%s"]
["git", "log", commit_range, "--pretty=format:%s"], cwd=cwd
)
commit_lines = [
@@ -967,14 +1011,21 @@ def _generate_release_notes(
]
commits = "\n".join(commit_lines)
else:
commit_range, commits = get_commits_from_last_tag(tag_name, version)
commit_range, commits = get_commits_from_last_tag(
tag_name, version, cwd=cwd
)
except subprocess.CalledProcessError:
commit_range, commits = get_commits_from_last_tag(tag_name, version)
commit_range, commits = get_commits_from_last_tag(
tag_name, version, cwd=cwd
)
github_contributors = get_github_contributors(commit_range)
github_contributors = get_github_contributors(
commit_range, repo=gh_repo, cwd=cwd
)
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
if openai_client is None:
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
if commits.strip():
contributors_section = ""
@@ -1532,7 +1583,13 @@ def _wait_for_pr_merged(branch: str, cwd: Path) -> None:
time.sleep(_PR_MERGE_POLL_INTERVAL)
def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> None:
def _release_enterprise(
version: str,
is_prerelease: bool,
dry_run: bool,
no_edit: bool = False,
openai_client: OpenAI | None = None,
) -> None:
"""Clone the enterprise repo, bump versions, and create a release PR.
Expects ENTERPRISE_REPO, ENTERPRISE_VERSION_DIRS, and
@@ -1542,6 +1599,8 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
version: New version string.
is_prerelease: Whether this is a pre-release version.
dry_run: Show what would be done without making changes.
no_edit: Skip the interactive release-notes edit prompt.
openai_client: Reuse OpenAI client from earlier phases if available.
"""
if (
not _ENTERPRISE_REPO
@@ -1559,7 +1618,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
)
if dry_run:
console.print(f"[dim][DRY RUN][/dim] Would clone {enterprise_repo}")
for d in _ENTERPRISE_VERSION_DIRS:
console.print(f"[dim][DRY RUN][/dim] Would update versions in {d}")
console.print(
@@ -1570,6 +1628,26 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
"[dim][DRY RUN][/dim] Would create bump PR, wait for merge, "
"then tag and release"
)
with tempfile.TemporaryDirectory() as tmp:
repo_dir = Path(tmp) / enterprise_repo.split("/")[-1]
console.print(f"\nCloning {enterprise_repo} (read-only preview)...")
run_command(["gh", "repo", "clone", enterprise_repo, str(repo_dir)])
console.print(f"[green]✓[/green] Cloned {enterprise_repo}")
_generate_release_notes(
version,
version,
no_edit,
cwd=repo_dir,
gh_repo=enterprise_repo,
openai_client=openai_client,
bump_already_done=False,
)
console.print(
"[dim][DRY RUN][/dim] Would tag and create GitHub release "
"with the notes above"
)
return
with tempfile.TemporaryDirectory() as tmp:
@@ -1682,8 +1760,18 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
run_command(["git", "pull"], cwd=repo_dir)
tag_name = version
release_notes, _, _ = _generate_release_notes(
version,
tag_name,
no_edit,
cwd=repo_dir,
gh_repo=enterprise_repo,
openai_client=openai_client,
)
run_command(
["git", "tag", "-a", tag_name, "-m", f"Release {version}"],
["git", "tag", "-a", tag_name, "-m", release_notes],
cwd=repo_dir,
)
run_command(["git", "push", "origin", tag_name], cwd=repo_dir)
@@ -1699,7 +1787,7 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
"--title",
tag_name,
"--notes",
f"Release {version}",
release_notes,
]
if is_prerelease:
gh_cmd.append("--prerelease")
@@ -1998,7 +2086,7 @@ def tag(dry_run: bool, no_edit: bool) -> None:
console.print("[green]✓[/green] main branch up to date")
release_notes, openai_client, is_prerelease = _generate_release_notes(
version, tag_name, no_edit
version, tag_name, no_edit, bump_already_done=True
)
docs_branch = _update_docs_and_create_pr(
@@ -2109,7 +2197,7 @@ def release(
if skip_to_enterprise:
try:
_release_enterprise(version, is_prerelease, dry_run)
_release_enterprise(version, is_prerelease, dry_run, no_edit=no_edit)
except BaseException as e:
_print_release_error(e)
_resume_hint(
@@ -2205,7 +2293,7 @@ def release(
console.print("[green]✓[/green] main branch up to date")
release_notes, openai_client, is_prerelease = _generate_release_notes(
version, tag_name, no_edit
version, tag_name, no_edit, bump_already_done=not dry_run
)
docs_branch = _update_docs_and_create_pr(
@@ -2259,7 +2347,13 @@ def release(
if not skip_enterprise:
try:
_release_enterprise(version, is_prerelease, dry_run)
_release_enterprise(
version,
is_prerelease,
dry_run,
no_edit=no_edit,
openai_client=openai_client,
)
except BaseException as e:
_print_release_error(e)
_resume_hint(

View File

@@ -282,6 +282,25 @@ class TestUpdatePyprojectDependencies:
assert '"crewai-files==2.0.0"' in result
assert '"requests>=2.0"' in result
def test_skips_crewai_files_in_file_processing_extra(self, tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
dedent("""\
[project.optional-dependencies]
file-processing = [
"crewai-files==1.0.0",
]
other = [
"crewai-files==1.0.0",
]
""")
)
update_pyproject_dependencies(pyproject, "2.0.0")
result = pyproject.read_text()
assert '"crewai-files==1.0.0"' in result
assert '"crewai-files==2.0.0"' in result
def test_leaves_bare_crewai_pin_alone(self, tmp_path: Path) -> None:
"""`crewai==` must not collide with `crewai-core==` etc."""
pyproject = tmp_path / "pyproject.toml"

View File

@@ -185,7 +185,7 @@ exclude-newer = "3 days"
# python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers).
# gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath).
# urllib3 <2.7.0 has GHSA-qccp-gfcp-xxvc (ProxyManager cross-origin redirect leaks Authorization/Cookie) and GHSA-mf9v-mfxr-j63j (streaming decompression-bomb bypass); force 2.7.0+.
# langsmith <0.7.31 has GHSA-rr7j-v2q5-chgv (streaming token redaction bypass); force 0.7.31+.
# langsmith <0.8.0 has GHSA-3644-q5cj-c5c7 (public prompt manifest deserialization, SSRF/secret disclosure); force 0.8.0+.
# authlib <1.6.11 has GHSA-jj8c-mmj3-mmgv (CSRF bypass in cache-based state storage).
# litellm 1.83.8+ hard-pins openai==2.24.0, missing openai.types.responses used by crewai;
# override to >=2.30.0 (the version litellm 1.83.7 used) until upstream relaxes the pin.
@@ -203,7 +203,7 @@ override-dependencies = [
"uv>=0.11.6,<1",
"python-multipart>=0.0.27,<1",
"gitpython>=3.1.50,<4",
"langsmith>=0.7.31,<0.8",
"langsmith>=0.8.0,<1",
"authlib>=1.6.11",
]

16
uv.lock generated
View File

@@ -13,7 +13,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-05-08T16:33:02.834109Z"
exclude-newer = "2026-05-16T15:32:24.373474Z"
exclude-newer-span = "P3D"
[manifest]
@@ -31,7 +31,7 @@ overrides = [
{ name = "gitpython", specifier = ">=3.1.50,<4" },
{ name = "langchain-core", specifier = ">=1.3.3,<2" },
{ name = "langchain-text-splitters", specifier = ">=1.1.2,<2" },
{ name = "langsmith", specifier = ">=0.7.31,<0.8" },
{ name = "langsmith", specifier = ">=0.8.0,<1" },
{ name = "onnxruntime", marker = "python_full_version < '3.11'", specifier = "<1.24" },
{ name = "openai", specifier = ">=2.30.0,<3" },
{ name = "pillow", specifier = ">=12.1.1" },
@@ -3268,11 +3268,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.11"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -3888,7 +3888,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2
[[package]]
name = "langsmith"
version = "0.7.32"
version = "0.8.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -3901,9 +3901,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2f/b4/a0b4a501bee6b8a741ce29f8c48155b132118483cddc6f9247735ddb38fa/langsmith-0.7.32.tar.gz", hash = "sha256:b59b8e106d0e4c4842e158229296086e2aa7c561e3f602acda73d3ad0062e915", size = 1184518, upload-time = "2026-04-15T23:42:41.885Z" }
sdist = { url = "https://files.pythonhosted.org/packages/de/8a/1e8ea5e8bab2a65fa95bd36229ef38e8723ec46e430e20ca2d953487a7f1/langsmith-0.8.3.tar.gz", hash = "sha256:767ff7a8d136ed42926bf99059ac631dc6883542d6e3104b32e71c7625e1fa05", size = 4460330, upload-time = "2026-05-07T19:56:56.18Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/bc/148f98ac7dad73ac5e1b1c985290079cfeeb9ba13d760a24f25002beb2c9/langsmith-0.7.32-py3-none-any.whl", hash = "sha256:e1fde928990c4c52f47dc5132708cec674355d9101723d564183e965f383bf5f", size = 378272, upload-time = "2026-04-15T23:42:39.905Z" },
{ url = "https://files.pythonhosted.org/packages/98/a9/51e644c1f1dbc3dd7d22dfd6412eab206d538c81e024e4f287373544bdcb/langsmith-0.8.3-py3-none-any.whl", hash = "sha256:b2e40e308222fa0beb2dccee3b4b30bfee9062d7a4f20a3e3e93df3c51a08ab4", size = 399048, upload-time = "2026-05-07T19:56:53.994Z" },
]
[[package]]