mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-06-25 10:08:11 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
178c2d212c | ||
|
|
563b55f7ca | ||
|
|
340d23ae5d | ||
|
|
7738a1d30c | ||
|
|
156b3500b4 | ||
|
|
5827abbc17 |
@@ -397,8 +397,14 @@
|
||||
"group": "Build",
|
||||
"pages": [
|
||||
"edge/en/enterprise/features/automations",
|
||||
"edge/en/enterprise/features/crew-studio",
|
||||
"edge/en/enterprise/features/merged-step-card",
|
||||
{
|
||||
"group": "Crew Studio",
|
||||
"icon": "pencil",
|
||||
"pages": [
|
||||
"edge/en/enterprise/features/crew-studio",
|
||||
"edge/en/enterprise/features/merged-step-card"
|
||||
]
|
||||
},
|
||||
"edge/en/enterprise/features/marketplace",
|
||||
"edge/en/enterprise/features/agent-repositories",
|
||||
"edge/en/enterprise/features/tools-and-integrations",
|
||||
@@ -922,8 +928,14 @@
|
||||
"group": "Build",
|
||||
"pages": [
|
||||
"v1.14.7/en/enterprise/features/automations",
|
||||
"v1.14.7/en/enterprise/features/crew-studio",
|
||||
"v1.14.7/en/enterprise/features/merged-step-card",
|
||||
{
|
||||
"group": "Crew Studio",
|
||||
"icon": "pencil",
|
||||
"pages": [
|
||||
"v1.14.7/en/enterprise/features/crew-studio",
|
||||
"v1.14.7/en/enterprise/features/merged-step-card"
|
||||
]
|
||||
},
|
||||
"v1.14.7/en/enterprise/features/marketplace",
|
||||
"v1.14.7/en/enterprise/features/agent-repositories",
|
||||
"v1.14.7/en/enterprise/features/tools-and-integrations",
|
||||
@@ -8549,8 +8561,14 @@
|
||||
"group": "Construir",
|
||||
"pages": [
|
||||
"edge/pt-BR/enterprise/features/automations",
|
||||
"edge/pt-BR/enterprise/features/crew-studio",
|
||||
"edge/pt-BR/enterprise/features/merged-step-card",
|
||||
{
|
||||
"group": "Crew Studio",
|
||||
"icon": "pencil",
|
||||
"pages": [
|
||||
"edge/pt-BR/enterprise/features/crew-studio",
|
||||
"edge/pt-BR/enterprise/features/merged-step-card"
|
||||
]
|
||||
},
|
||||
"edge/pt-BR/enterprise/features/marketplace",
|
||||
"edge/pt-BR/enterprise/features/agent-repositories",
|
||||
"edge/pt-BR/enterprise/features/tools-and-integrations",
|
||||
@@ -9051,8 +9069,14 @@
|
||||
"group": "Construir",
|
||||
"pages": [
|
||||
"v1.14.7/pt-BR/enterprise/features/automations",
|
||||
"v1.14.7/pt-BR/enterprise/features/crew-studio",
|
||||
"v1.14.7/pt-BR/enterprise/features/merged-step-card",
|
||||
{
|
||||
"group": "Crew Studio",
|
||||
"icon": "pencil",
|
||||
"pages": [
|
||||
"v1.14.7/pt-BR/enterprise/features/crew-studio",
|
||||
"v1.14.7/pt-BR/enterprise/features/merged-step-card"
|
||||
]
|
||||
},
|
||||
"v1.14.7/pt-BR/enterprise/features/marketplace",
|
||||
"v1.14.7/pt-BR/enterprise/features/agent-repositories",
|
||||
"v1.14.7/pt-BR/enterprise/features/tools-and-integrations",
|
||||
@@ -16415,8 +16439,14 @@
|
||||
"group": "빌드",
|
||||
"pages": [
|
||||
"edge/ko/enterprise/features/automations",
|
||||
"edge/ko/enterprise/features/crew-studio",
|
||||
"edge/ko/enterprise/features/merged-step-card",
|
||||
{
|
||||
"group": "Crew Studio",
|
||||
"icon": "pencil",
|
||||
"pages": [
|
||||
"edge/ko/enterprise/features/crew-studio",
|
||||
"edge/ko/enterprise/features/merged-step-card"
|
||||
]
|
||||
},
|
||||
"edge/ko/enterprise/features/marketplace",
|
||||
"edge/ko/enterprise/features/agent-repositories",
|
||||
"edge/ko/enterprise/features/tools-and-integrations",
|
||||
@@ -16929,8 +16959,14 @@
|
||||
"group": "빌드",
|
||||
"pages": [
|
||||
"v1.14.7/ko/enterprise/features/automations",
|
||||
"v1.14.7/ko/enterprise/features/crew-studio",
|
||||
"v1.14.7/ko/enterprise/features/merged-step-card",
|
||||
{
|
||||
"group": "Crew Studio",
|
||||
"icon": "pencil",
|
||||
"pages": [
|
||||
"v1.14.7/ko/enterprise/features/crew-studio",
|
||||
"v1.14.7/ko/enterprise/features/merged-step-card"
|
||||
]
|
||||
},
|
||||
"v1.14.7/ko/enterprise/features/marketplace",
|
||||
"v1.14.7/ko/enterprise/features/agent-repositories",
|
||||
"v1.14.7/ko/enterprise/features/tools-and-integrations",
|
||||
@@ -24473,8 +24509,14 @@
|
||||
"group": "البناء",
|
||||
"pages": [
|
||||
"edge/ar/enterprise/features/automations",
|
||||
"edge/ar/enterprise/features/crew-studio",
|
||||
"edge/ar/enterprise/features/merged-step-card",
|
||||
{
|
||||
"group": "استوديو الطاقم",
|
||||
"icon": "pencil",
|
||||
"pages": [
|
||||
"edge/ar/enterprise/features/crew-studio",
|
||||
"edge/ar/enterprise/features/merged-step-card"
|
||||
]
|
||||
},
|
||||
"edge/ar/enterprise/features/marketplace",
|
||||
"edge/ar/enterprise/features/agent-repositories",
|
||||
"edge/ar/enterprise/features/tools-and-integrations",
|
||||
@@ -24987,8 +25029,14 @@
|
||||
"group": "البناء",
|
||||
"pages": [
|
||||
"v1.14.7/ar/enterprise/features/automations",
|
||||
"v1.14.7/ar/enterprise/features/crew-studio",
|
||||
"v1.14.7/ar/enterprise/features/merged-step-card",
|
||||
{
|
||||
"group": "استوديو الطاقم",
|
||||
"icon": "pencil",
|
||||
"pages": [
|
||||
"v1.14.7/ar/enterprise/features/crew-studio",
|
||||
"v1.14.7/ar/enterprise/features/merged-step-card"
|
||||
]
|
||||
},
|
||||
"v1.14.7/ar/enterprise/features/marketplace",
|
||||
"v1.14.7/ar/enterprise/features/agent-repositories",
|
||||
"v1.14.7/ar/enterprise/features/tools-and-integrations",
|
||||
|
||||
@@ -4,6 +4,32 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="24 يونيو 2026">
|
||||
## v1.14.8a5
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a5)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- جعل المراجع التصريحية تعمل عبر التدفقات والفرق (#6326)
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح مدخلات بدء حالة تدفق مخطط JSON (#6325)
|
||||
|
||||
### الوثائق
|
||||
- وضع بطاقة واحدة لكل خطوة تحت استوديو الفريق وإزالة لافتة التوزيع (AGE-107) (#6317)
|
||||
- تحديث اللقطة وسجل التغييرات للإصدار v1.14.8a4 (#6319)
|
||||
|
||||
### إعادة الهيكلة
|
||||
- إزالة `StateProxy` من الوصول إلى حالة التدفق (#6327)
|
||||
|
||||
## المساهمون
|
||||
|
||||
@jessemiller, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="24 يونيو 2026">
|
||||
## v1.14.8a4
|
||||
|
||||
|
||||
@@ -5,11 +5,6 @@ icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**الإطلاق يوم الأربعاء 24 يونيو.** تنتقل لوحة Studio إلى بطاقة واحدة لكل خطوة بدلاً من عُقد منفصلة للمهمة والوكيل، وذلك لتبسيط اللوحة مع إضافتنا لوظائف جديدة قريبًا. تستمر أتمتتك الحالية في العمل دون أي تغييرات مطلوبة — تبقى جميع إعدادات المهمة والوكيل متاحة، ولكن منظّمة في بطاقة واحدة.
|
||||
</Note>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
على لوحة Studio، تُمثَّل كل خطوة عمل بـ **بطاقة واحدة**. تجمع البطاقة بين عنصرين كانا في السابق في عُقد منفصلة:
|
||||
|
||||
@@ -4,6 +4,32 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Jun 24, 2026">
|
||||
## v1.14.8a5
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a5)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Make declarative refs work across flows and crews (#6326)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix JSON schema flow state kickoff inputs (#6325)
|
||||
|
||||
### Documentation
|
||||
- Nest One Card per Step under Crew Studio and drop rollout banner (AGE-107) (#6317)
|
||||
- Update snapshot and changelog for v1.14.8a4 (#6319)
|
||||
|
||||
### Refactoring
|
||||
- Remove `StateProxy` from flow state access (#6327)
|
||||
|
||||
## Contributors
|
||||
|
||||
@jessemiller, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Jun 24, 2026">
|
||||
## v1.14.8a4
|
||||
|
||||
|
||||
@@ -5,11 +5,6 @@ icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Rolling out Wednesday, June 24th.** The Studio canvas is moving to one card per step instead of separate task and agent nodes, to streamline the canvas as we add new functionality soon. Your existing automations keep working with no changes needed — every task and agent setting is still available, just organized onto a single card.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
On the Studio canvas, each step of work is represented by a **single card**. The card combines two things that used to live in separate nodes:
|
||||
|
||||
@@ -4,6 +4,32 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 6월 24일">
|
||||
## v1.14.8a5
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a5)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 선언적 참조가 흐름과 크루 간에 작동하도록 수정 (#6326)
|
||||
|
||||
### 버그 수정
|
||||
- JSON 스키마 흐름 상태 시작 입력 수정 (#6325)
|
||||
|
||||
### 문서
|
||||
- Crew Studio 아래에 단계별 One Card를 중첩하고 롤아웃 배너 제거 (AGE-107) (#6317)
|
||||
- v1.14.8a4의 스냅샷 및 변경 로그 업데이트 (#6319)
|
||||
|
||||
### 리팩토링
|
||||
- 흐름 상태 접근에서 `StateProxy` 제거 (#6327)
|
||||
|
||||
## 기여자
|
||||
|
||||
@jessemiller, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 6월 24일">
|
||||
## v1.14.8a4
|
||||
|
||||
|
||||
@@ -5,11 +5,6 @@ icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**6월 24일 수요일 출시.** Studio 캔버스가 작업과 에이전트를 별도의 노드로 표시하는 대신 단계당 하나의 카드로 전환됩니다. 곧 추가될 새로운 기능을 위해 캔버스를 간소화하기 위한 변경입니다. 기존 자동화는 아무런 변경 없이 그대로 동작하며, 모든 작업 및 에이전트 설정은 단일 카드에 정리되어 그대로 사용할 수 있습니다.
|
||||
</Note>
|
||||
|
||||
## 개요
|
||||
|
||||
Studio 캔버스에서 각 작업 단계는 **하나의 카드**로 표현됩니다. 이 카드는 이전에 별도의 노드에 있던 두 가지를 결합합니다:
|
||||
|
||||
@@ -4,6 +4,32 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="24 jun 2026">
|
||||
## v1.14.8a5
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a5)
|
||||
|
||||
## O que mudou
|
||||
|
||||
### Recursos
|
||||
- Fazer referências declarativas funcionarem em diferentes fluxos e equipes (#6326)
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir entradas de kickoff de estado de fluxo do esquema JSON (#6325)
|
||||
|
||||
### Documentação
|
||||
- Aninhar One Card por Etapa sob Crew Studio e remover banner de rollout (AGE-107) (#6317)
|
||||
- Atualizar snapshot e changelog para v1.14.8a4 (#6319)
|
||||
|
||||
### Refatoração
|
||||
- Remover `StateProxy` do acesso ao estado de fluxo (#6327)
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@jessemiller, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="24 jun 2026">
|
||||
## v1.14.8a4
|
||||
|
||||
|
||||
@@ -5,11 +5,6 @@ icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Lançamento na quarta-feira, 24 de junho.** O canvas do Studio passa a exibir um card por etapa, em vez de nós separados para tarefa e agente, para simplificar o canvas à medida que adicionamos novas funcionalidades em breve. Suas automações existentes continuam funcionando sem nenhuma alteração necessária — cada configuração de tarefa e de agente continua disponível, apenas organizada em um único card.
|
||||
</Note>
|
||||
|
||||
## Visão geral
|
||||
|
||||
No canvas do Studio, cada etapa de trabalho é representada por um **único card**. O card combina dois elementos que antes ficavam em nós separados:
|
||||
|
||||
@@ -5,11 +5,6 @@ icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**الإطلاق يوم الأربعاء 24 يونيو.** تنتقل لوحة Studio إلى بطاقة واحدة لكل خطوة بدلاً من عُقد منفصلة للمهمة والوكيل، وذلك لتبسيط اللوحة مع إضافتنا لوظائف جديدة قريبًا. تستمر أتمتتك الحالية في العمل دون أي تغييرات مطلوبة — تبقى جميع إعدادات المهمة والوكيل متاحة، ولكن منظّمة في بطاقة واحدة.
|
||||
</Note>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
على لوحة Studio، تُمثَّل كل خطوة عمل بـ **بطاقة واحدة**. تجمع البطاقة بين عنصرين كانا في السابق في عُقد منفصلة:
|
||||
|
||||
@@ -5,11 +5,6 @@ icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Rolling out Wednesday, June 24th.** The Studio canvas is moving to one card per step instead of separate task and agent nodes, to streamline the canvas as we add new functionality soon. Your existing automations keep working with no changes needed — every task and agent setting is still available, just organized onto a single card.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
On the Studio canvas, each step of work is represented by a **single card**. The card combines two things that used to live in separate nodes:
|
||||
|
||||
@@ -5,11 +5,6 @@ icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**6월 24일 수요일 출시.** Studio 캔버스가 작업과 에이전트를 별도의 노드로 표시하는 대신 단계당 하나의 카드로 전환됩니다. 곧 추가될 새로운 기능을 위해 캔버스를 간소화하기 위한 변경입니다. 기존 자동화는 아무런 변경 없이 그대로 동작하며, 모든 작업 및 에이전트 설정은 단일 카드에 정리되어 그대로 사용할 수 있습니다.
|
||||
</Note>
|
||||
|
||||
## 개요
|
||||
|
||||
Studio 캔버스에서 각 작업 단계는 **하나의 카드**로 표현됩니다. 이 카드는 이전에 별도의 노드에 있던 두 가지를 결합합니다:
|
||||
|
||||
@@ -5,11 +5,6 @@ icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Lançamento na quarta-feira, 24 de junho.** O canvas do Studio passa a exibir um card por etapa, em vez de nós separados para tarefa e agente, para simplificar o canvas à medida que adicionamos novas funcionalidades em breve. Suas automações existentes continuam funcionando sem nenhuma alteração necessária — cada configuração de tarefa e de agente continua disponível, apenas organizada em um único card.
|
||||
</Note>
|
||||
|
||||
## Visão geral
|
||||
|
||||
No canvas do Studio, cada etapa de trabalho é representada por um **único card**. O card combina dois elementos que antes ficavam em nós separados:
|
||||
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.8a4",
|
||||
"crewai-core==1.14.8a5",
|
||||
"click>=8.1.7,<9",
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"pydantic-settings~=2.10.1",
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a5"
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a4"
|
||||
"crewai[tools]==1.14.8a5"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a4"
|
||||
"crewai[tools]==1.14.8a5"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a4"
|
||||
"crewai[tools]==1.14.8a5"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a4"
|
||||
"crewai[tools]==1.14.8a5"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a5"
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a5"
|
||||
|
||||
@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests>=2.33.0,<3",
|
||||
"crewai==1.14.8a4",
|
||||
"crewai==1.14.8a5",
|
||||
"tiktoken>=0.8.0,<0.13",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -330,4 +330,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a5"
|
||||
|
||||
@@ -8,8 +8,8 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.8a4",
|
||||
"crewai-cli==1.14.8a4",
|
||||
"crewai-core==1.14.8a5",
|
||||
"crewai-cli==1.14.8a5",
|
||||
# Core Dependencies
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"openai>=2.30.0,<3",
|
||||
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.14.8a4",
|
||||
"crewai-tools==1.14.8a5",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken>=0.8.0,<0.13"
|
||||
|
||||
@@ -48,7 +48,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a5"
|
||||
|
||||
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"Memory": ("crewai.memory.unified_memory", "Memory"),
|
||||
|
||||
@@ -54,7 +54,7 @@ from crewai.events.types.tool_usage_events import (
|
||||
ToolUsageFinishedEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai.flow.flow import Flow, StateProxy, listen, or_, router, start
|
||||
from crewai.flow.flow import Flow, listen, or_, router, start
|
||||
from crewai.flow.types import FlowMethodName
|
||||
from crewai.hooks.llm_hooks import (
|
||||
get_after_llm_call_hooks,
|
||||
@@ -276,11 +276,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
"""
|
||||
return self.llm.supports_stop_words() if self.llm else False
|
||||
|
||||
@property
|
||||
def state(self) -> AgentExecutorState:
|
||||
"""Get thread-safe state proxy."""
|
||||
return StateProxy(self._state, self._state_lock) # type: ignore[return-value]
|
||||
|
||||
@property # type: ignore[misc]
|
||||
def iterations(self) -> int:
|
||||
"""Compatibility property for mixin - returns state iterations."""
|
||||
|
||||
@@ -24,9 +24,6 @@ from crewai.flow.runtime import (
|
||||
Flow as RuntimeFlow,
|
||||
FlowMeta,
|
||||
FlowState,
|
||||
LockedDictProxy,
|
||||
LockedListProxy,
|
||||
StateProxy,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,9 +39,6 @@ __all__ = [
|
||||
"Flow",
|
||||
"FlowMeta",
|
||||
"FlowState",
|
||||
"LockedDictProxy",
|
||||
"LockedListProxy",
|
||||
"StateProxy",
|
||||
"and_",
|
||||
"listen",
|
||||
"or_",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Flow Runtime: the engine that executes a Flow.
|
||||
|
||||
Provides the ``Flow`` class (kickoff/resume/listener dispatch), the
|
||||
``FlowMeta`` metaclass, and the thread-safe state proxies. Flows
|
||||
authored with the Python DSL (see ``dsl``) are described by a Flow
|
||||
Provides the ``Flow`` class (kickoff/resume/listener dispatch) and the
|
||||
``FlowMeta`` metaclass. Flows authored with the Python DSL (see ``dsl``)
|
||||
are described by a Flow
|
||||
Structure (see ``flow_definition``) and executed here.
|
||||
"""
|
||||
|
||||
@@ -11,12 +11,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
ItemsView,
|
||||
Iterable,
|
||||
Iterator,
|
||||
KeysView,
|
||||
Sequence,
|
||||
ValuesView,
|
||||
)
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
import contextvars
|
||||
@@ -35,10 +31,8 @@ from typing import (
|
||||
Generic,
|
||||
Literal,
|
||||
ParamSpec,
|
||||
SupportsIndex,
|
||||
TypeVar,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -123,7 +117,6 @@ from crewai.flow.human_feedback import (
|
||||
from crewai.flow.input_provider import InputProvider
|
||||
from crewai.flow.persistence.base import FlowPersistence
|
||||
from crewai.flow.runtime._actions import FlowScriptExecutionDisabledError, build_action
|
||||
from crewai.flow.runtime._refs import resolve_instance_ref, resolve_ref
|
||||
from crewai.flow.types import (
|
||||
FlowExecutionData,
|
||||
FlowMethodName,
|
||||
@@ -137,6 +130,7 @@ from crewai.state.checkpoint_config import (
|
||||
_coerce_checkpoint,
|
||||
apply_checkpoint,
|
||||
)
|
||||
from crewai.utilities.declarative_refs import InvalidRefError, resolve_ref
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -227,7 +221,12 @@ def _build_definition_state_model(
|
||||
pass
|
||||
|
||||
model_class = StateWithId
|
||||
return model_class(**kwargs)
|
||||
try:
|
||||
return model_class(**kwargs)
|
||||
except ValidationError as e:
|
||||
if any(error.get("type") != "missing" for error in e.errors()):
|
||||
raise
|
||||
return model_class.model_construct(**kwargs)
|
||||
|
||||
|
||||
def _iter_condition_events(condition: FlowDefinitionCondition) -> Iterator[str]:
|
||||
@@ -284,6 +283,18 @@ def _resolve_persistence(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_instance_ref(ref: str, *, field: str) -> Any:
|
||||
target = resolve_ref(ref, field=field)
|
||||
if not inspect.isclass(target):
|
||||
return target
|
||||
try:
|
||||
return target()
|
||||
except Exception as e:
|
||||
raise InvalidRefError(
|
||||
f"cannot instantiate {field} ref {ref!r} without arguments: {e}"
|
||||
) from e
|
||||
|
||||
|
||||
def _serialize_persistence(value: Any) -> dict[str, Any] | None:
|
||||
if value is None:
|
||||
return None
|
||||
@@ -299,7 +310,7 @@ def _validate_input_provider(value: Any) -> Any:
|
||||
if value is None or isinstance(value, InputProvider):
|
||||
return value
|
||||
if isinstance(value, str) and ":" in value:
|
||||
resolved = resolve_instance_ref(value, field="input_provider")
|
||||
resolved = _resolve_instance_ref(value, field="input_provider")
|
||||
else:
|
||||
from crewai.types.callback import _dotted_path_to_instance
|
||||
|
||||
@@ -366,304 +377,6 @@ R = TypeVar("R")
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
class LockedListProxy(list, Generic[T]): # type: ignore[type-arg]
|
||||
"""Thread-safe proxy for list operations.
|
||||
|
||||
Subclasses ``list`` so that ``isinstance(proxy, list)`` returns True,
|
||||
which is required by libraries like LanceDB and Pydantic that do strict
|
||||
type checks. All mutations go through the lock; reads delegate to the
|
||||
underlying list.
|
||||
"""
|
||||
|
||||
def __init__(self, lst: list[T], lock: threading.Lock) -> None:
|
||||
super().__init__() # empty builtin list; all access goes through self._list
|
||||
self._list = lst
|
||||
self._lock = lock
|
||||
|
||||
def append(self, item: T) -> None:
|
||||
with self._lock:
|
||||
self._list.append(item)
|
||||
|
||||
def extend(self, items: Iterable[T]) -> None:
|
||||
with self._lock:
|
||||
self._list.extend(items)
|
||||
|
||||
def insert(self, index: SupportsIndex, item: T) -> None:
|
||||
with self._lock:
|
||||
self._list.insert(index, item)
|
||||
|
||||
def remove(self, item: T) -> None:
|
||||
with self._lock:
|
||||
self._list.remove(item)
|
||||
|
||||
def pop(self, index: SupportsIndex = -1) -> T:
|
||||
with self._lock:
|
||||
return self._list.pop(index)
|
||||
|
||||
def clear(self) -> None:
|
||||
with self._lock:
|
||||
self._list.clear()
|
||||
|
||||
@overload
|
||||
def __setitem__(self, index: SupportsIndex, value: T) -> None: ...
|
||||
@overload
|
||||
def __setitem__(self, index: slice, value: Iterable[T]) -> None: ...
|
||||
def __setitem__(self, index: Any, value: Any) -> None:
|
||||
with self._lock:
|
||||
self._list[index] = value
|
||||
|
||||
def __delitem__(self, index: SupportsIndex | slice) -> None:
|
||||
with self._lock:
|
||||
del self._list[index]
|
||||
|
||||
@overload
|
||||
def __getitem__(self, index: SupportsIndex) -> T: ...
|
||||
@overload
|
||||
def __getitem__(self, index: slice) -> list[T]: ...
|
||||
def __getitem__(self, index: Any) -> Any:
|
||||
return self._list[index]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._list)
|
||||
|
||||
def __iter__(self) -> Iterator[T]:
|
||||
return iter(self._list)
|
||||
|
||||
def __contains__(self, item: object) -> bool:
|
||||
return item in self._list
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self._list)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._list)
|
||||
|
||||
def index(
|
||||
self, value: T, start: SupportsIndex = 0, stop: SupportsIndex | None = None
|
||||
) -> int:
|
||||
if stop is None:
|
||||
return self._list.index(value, start)
|
||||
return self._list.index(value, start, stop)
|
||||
|
||||
def count(self, value: T) -> int:
|
||||
return self._list.count(value)
|
||||
|
||||
def sort(self, *, key: Any = None, reverse: bool = False) -> None:
|
||||
with self._lock:
|
||||
self._list.sort(key=key, reverse=reverse)
|
||||
|
||||
def reverse(self) -> None:
|
||||
with self._lock:
|
||||
self._list.reverse()
|
||||
|
||||
def copy(self) -> list[T]:
|
||||
return self._list.copy()
|
||||
|
||||
def __add__(self, other: list[T]) -> list[T]: # type: ignore[override]
|
||||
return self._list + other
|
||||
|
||||
def __radd__(self, other: list[T]) -> list[T]:
|
||||
return other + self._list
|
||||
|
||||
def __iadd__(self, other: Iterable[T]) -> LockedListProxy[T]: # type: ignore[override]
|
||||
with self._lock:
|
||||
self._list += list(other)
|
||||
return self
|
||||
|
||||
def __mul__(self, n: SupportsIndex) -> list[T]:
|
||||
return self._list * n
|
||||
|
||||
def __rmul__(self, n: SupportsIndex) -> list[T]:
|
||||
return self._list * n
|
||||
|
||||
def __imul__(self, n: SupportsIndex) -> LockedListProxy[T]:
|
||||
with self._lock:
|
||||
self._list *= n
|
||||
return self
|
||||
|
||||
def __reversed__(self) -> Iterator[T]:
|
||||
return reversed(self._list)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Compare based on the underlying list contents."""
|
||||
if isinstance(other, LockedListProxy):
|
||||
# Avoid deadlocks by acquiring locks in a consistent order.
|
||||
first, second = (self, other) if id(self) <= id(other) else (other, self)
|
||||
with first._lock:
|
||||
with second._lock:
|
||||
return first._list == second._list
|
||||
with self._lock:
|
||||
return self._list == other
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class LockedDictProxy(dict, Generic[T]): # type: ignore[type-arg]
|
||||
"""Thread-safe proxy for dict operations.
|
||||
|
||||
Subclasses ``dict`` so that ``isinstance(proxy, dict)`` returns True,
|
||||
which is required by libraries like Pydantic that do strict type checks.
|
||||
All mutations go through the lock; reads delegate to the underlying dict.
|
||||
"""
|
||||
|
||||
def __init__(self, d: dict[str, T], lock: threading.Lock) -> None:
|
||||
super().__init__() # empty builtin dict; all access goes through self._dict
|
||||
self._dict = d
|
||||
self._lock = lock
|
||||
|
||||
def __setitem__(self, key: str, value: T) -> None:
|
||||
with self._lock:
|
||||
self._dict[key] = value
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
with self._lock:
|
||||
del self._dict[key]
|
||||
|
||||
def pop(self, key: str, *default: T) -> T: # type: ignore[override]
|
||||
with self._lock:
|
||||
return self._dict.pop(key, *default)
|
||||
|
||||
def update(self, other: dict[str, T]) -> None: # type: ignore[override]
|
||||
with self._lock:
|
||||
self._dict.update(other)
|
||||
|
||||
def clear(self) -> None:
|
||||
with self._lock:
|
||||
self._dict.clear()
|
||||
|
||||
def setdefault(self, key: str, default: T) -> T: # type: ignore[override]
|
||||
with self._lock:
|
||||
return self._dict.setdefault(key, default)
|
||||
|
||||
def __getitem__(self, key: str) -> T:
|
||||
return self._dict[key]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._dict)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self._dict)
|
||||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
return key in self._dict
|
||||
|
||||
def keys(self) -> KeysView[str]: # type: ignore[override]
|
||||
return self._dict.keys()
|
||||
|
||||
def values(self) -> ValuesView[T]: # type: ignore[override]
|
||||
return self._dict.values()
|
||||
|
||||
def items(self) -> ItemsView[str, T]: # type: ignore[override]
|
||||
return self._dict.items()
|
||||
|
||||
def get(self, key: str, default: T | None = None) -> T | None: # type: ignore[override]
|
||||
return self._dict.get(key, default)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self._dict)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._dict)
|
||||
|
||||
def copy(self) -> dict[str, T]:
|
||||
return self._dict.copy()
|
||||
|
||||
def __or__(self, other: dict[str, T]) -> dict[str, T]: # type: ignore[override]
|
||||
return self._dict | other
|
||||
|
||||
def __ror__(self, other: dict[str, T]) -> dict[str, T]: # type: ignore[override]
|
||||
return other | self._dict
|
||||
|
||||
def __ior__(self, other: dict[str, T]) -> LockedDictProxy[T]: # type: ignore[override]
|
||||
with self._lock:
|
||||
self._dict |= other
|
||||
return self
|
||||
|
||||
def __reversed__(self) -> Iterator[str]:
|
||||
return reversed(self._dict)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Compare based on the underlying dict contents."""
|
||||
if isinstance(other, LockedDictProxy):
|
||||
# Avoid deadlocks by acquiring locks in a consistent order.
|
||||
first, second = (self, other) if id(self) <= id(other) else (other, self)
|
||||
with first._lock:
|
||||
with second._lock:
|
||||
return first._dict == second._dict
|
||||
with self._lock:
|
||||
return self._dict == other
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class StateProxy(Generic[T]):
|
||||
"""Proxy that provides thread-safe access to flow state.
|
||||
|
||||
Wraps state objects (dict or BaseModel) and uses a lock for all write
|
||||
operations to prevent race conditions when parallel listeners modify state.
|
||||
"""
|
||||
|
||||
__slots__ = ("_proxy_lock", "_proxy_state")
|
||||
|
||||
def __init__(self, state: T, lock: threading.Lock) -> None:
|
||||
object.__setattr__(self, "_proxy_state", state)
|
||||
object.__setattr__(self, "_proxy_lock", lock)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
value = getattr(object.__getattribute__(self, "_proxy_state"), name)
|
||||
lock = object.__getattribute__(self, "_proxy_lock")
|
||||
if isinstance(value, list):
|
||||
return LockedListProxy(value, lock)
|
||||
if isinstance(value, dict):
|
||||
return LockedDictProxy(value, lock)
|
||||
return value
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if name in ("_proxy_state", "_proxy_lock"):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
if isinstance(value, LockedListProxy):
|
||||
value = value._list
|
||||
elif isinstance(value, LockedDictProxy):
|
||||
value = value._dict
|
||||
with object.__getattribute__(self, "_proxy_lock"):
|
||||
setattr(object.__getattribute__(self, "_proxy_state"), name, value)
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return object.__getattribute__(self, "_proxy_state")[key]
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
with object.__getattribute__(self, "_proxy_lock"):
|
||||
object.__getattribute__(self, "_proxy_state")[key] = value
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
with object.__getattribute__(self, "_proxy_lock"):
|
||||
del object.__getattribute__(self, "_proxy_state")[key]
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in object.__getattribute__(self, "_proxy_state")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(object.__getattribute__(self, "_proxy_state"))
|
||||
|
||||
def _unwrap(self) -> T:
|
||||
"""Return the underlying state object."""
|
||||
return cast(T, object.__getattribute__(self, "_proxy_state"))
|
||||
|
||||
def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Return state as a dictionary.
|
||||
|
||||
Works for both dict and BaseModel underlying states.
|
||||
"""
|
||||
state = object.__getattribute__(self, "_proxy_state")
|
||||
if isinstance(state, dict):
|
||||
return state
|
||||
result: dict[str, Any] = state.model_dump(*args, **kwargs)
|
||||
return result
|
||||
|
||||
|
||||
class FlowMeta(ModelMetaclass):
|
||||
def __new__(
|
||||
mcs,
|
||||
@@ -1008,7 +721,6 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
)
|
||||
_method_outputs: list[Any] = PrivateAttr(default_factory=list)
|
||||
_definition: FlowDefinition = PrivateAttr()
|
||||
_state_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
||||
_or_listeners_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
||||
_completed_methods: set[FlowMethodName] = PrivateAttr(default_factory=set)
|
||||
_method_call_counts: dict[FlowMethodName, int] = PrivateAttr(default_factory=dict)
|
||||
@@ -1930,7 +1642,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
|
||||
@property
|
||||
def state(self) -> T:
|
||||
return StateProxy(self._state, self._state_lock) # type: ignore[return-value]
|
||||
return cast(T, self._state)
|
||||
|
||||
@property
|
||||
def method_outputs(self) -> list[Any]:
|
||||
@@ -3600,7 +3312,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
) -> Any:
|
||||
provider = feedback_definition.provider
|
||||
if isinstance(provider, str):
|
||||
provider = resolve_instance_ref(provider, field="human_feedback.provider")
|
||||
provider = _resolve_instance_ref(provider, field="human_feedback.provider")
|
||||
if provider is None:
|
||||
from crewai.flow.flow_config import flow_config
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@ from crewai.flow.flow_definition import (
|
||||
FlowToolActionDefinition,
|
||||
)
|
||||
from crewai.flow.runtime._outputs import outputs_by_name
|
||||
from crewai.flow.runtime._refs import InvalidRefError, resolve_ref
|
||||
from crewai.utilities.declarative_refs import (
|
||||
InvalidRefError,
|
||||
resolve_class_ref,
|
||||
resolve_ref,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -103,16 +107,17 @@ class ToolAction:
|
||||
)
|
||||
|
||||
def _build_tool(self) -> Any:
|
||||
target = resolve_ref(self.definition.ref, field="do")
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
if not (inspect.isclass(target) and issubclass(target, BaseTool)):
|
||||
raise InvalidRefError(
|
||||
f"invalid tool ref {self.definition.ref!r}; expected a BaseTool class"
|
||||
)
|
||||
|
||||
tool_cls = cast(
|
||||
Callable[[], BaseTool],
|
||||
resolve_class_ref(
|
||||
self.definition.ref,
|
||||
field="do",
|
||||
base_class=BaseTool,
|
||||
),
|
||||
)
|
||||
try:
|
||||
tool_cls = cast(Callable[[], BaseTool], target)
|
||||
return tool_cls()
|
||||
except Exception as e:
|
||||
raise InvalidRefError(
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Resolution of ``module:qualname`` refs into live Python objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
from operator import attrgetter
|
||||
from typing import Any
|
||||
|
||||
|
||||
class InvalidRefError(ValueError):
|
||||
"""A definition ref that cannot be resolved to a live object."""
|
||||
|
||||
|
||||
def resolve_ref(ref: str, *, field: str) -> Any:
|
||||
"""Import the object a definition's `module:qualname` ref points to."""
|
||||
module_name, _, qualname = ref.partition(":")
|
||||
if "<" in ref or not module_name or not qualname:
|
||||
raise InvalidRefError(
|
||||
f"invalid {field} ref {ref!r}; expected 'module:qualname'"
|
||||
)
|
||||
try:
|
||||
return attrgetter(qualname)(importlib.import_module(module_name))
|
||||
except (ImportError, AttributeError) as e:
|
||||
raise InvalidRefError(f"unresolvable {field} ref {ref!r}") from e
|
||||
|
||||
|
||||
def resolve_instance_ref(ref: str, *, field: str) -> Any:
|
||||
"""Resolve a ref, auto-instantiating a no-arg class into an instance."""
|
||||
target = resolve_ref(ref, field=field)
|
||||
if not inspect.isclass(target):
|
||||
return target
|
||||
try:
|
||||
return target()
|
||||
except Exception as e:
|
||||
raise InvalidRefError(
|
||||
f"cannot instantiate {field} ref {ref!r} without arguments: {e}"
|
||||
) from e
|
||||
@@ -16,6 +16,8 @@ from urllib.parse import unquote, urlparse
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from crewai.utilities.declarative_refs import InvalidRefError, resolve_class_ref
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1820,6 +1822,9 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li
|
||||
if tool_def.startswith("custom:"):
|
||||
tools.append(_resolve_custom_tool(tool_def[7:], project_root=project_root))
|
||||
continue
|
||||
if ":" in tool_def:
|
||||
tools.append(_instantiate_tool_import_ref(tool_def))
|
||||
continue
|
||||
try:
|
||||
tool_cls = _find_tool_class(tool_def)
|
||||
except Exception as e:
|
||||
@@ -1827,8 +1832,10 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li
|
||||
if tool_cls is None:
|
||||
raise JSONProjectError(
|
||||
f"Unknown tool '{tool_def}'. Tool names must match a class from "
|
||||
f"the 'crewai_tools' package (e.g. 'SerperDevTool') or use the "
|
||||
f"'custom:<name>' prefix for a tool defined in tools/<name>.py."
|
||||
f"the 'crewai_tools' package (e.g. 'SerperDevTool'), use a "
|
||||
f"'module:ClassName' import ref (e.g. 'crewai_tools:SerperDevTool'), "
|
||||
f"or use the 'custom:<name>' prefix for a tool defined in "
|
||||
f"tools/<name>.py."
|
||||
)
|
||||
try:
|
||||
tools.append(tool_cls())
|
||||
@@ -1839,6 +1846,32 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li
|
||||
return tools
|
||||
|
||||
|
||||
def _instantiate_tool_import_ref(ref: str) -> Any:
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
try:
|
||||
tool_cls = cast(
|
||||
Callable[[], BaseTool],
|
||||
resolve_class_ref(ref, field="tool", base_class=BaseTool),
|
||||
)
|
||||
except InvalidRefError as e:
|
||||
message = str(e)
|
||||
if (
|
||||
message.startswith("unresolvable ")
|
||||
or "expected 'module:qualname'" in message
|
||||
):
|
||||
raise JSONProjectError(str(e)) from e
|
||||
raise JSONProjectError(
|
||||
f"invalid tool ref {ref!r}; expected a BaseTool class"
|
||||
) from e
|
||||
try:
|
||||
return tool_cls()
|
||||
except Exception as e:
|
||||
raise JSONProjectError(
|
||||
f"cannot instantiate tool ref {ref!r} without arguments: {e}"
|
||||
) from e
|
||||
|
||||
|
||||
_tool_class_cache: dict[str, type | None] = {}
|
||||
|
||||
|
||||
|
||||
69
lib/crewai/src/crewai/utilities/declarative_refs.py
Normal file
69
lib/crewai/src/crewai/utilities/declarative_refs.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Resolve Python refs used in project definitions.
|
||||
|
||||
A ref must use this form: ``module:qualname``. ``module`` must name a Python
|
||||
module we can import. ``qualname`` must name something inside that module. For
|
||||
example, ``crewai_tools:SerperDevTool`` imports ``crewai_tools`` and returns
|
||||
``SerperDevTool`` from it. Dots in ``qualname`` mean nested attributes.
|
||||
|
||||
Examples:
|
||||
|
||||
- ``crewai_tools:SerperDevTool`` imports ``crewai_tools`` and returns
|
||||
``SerperDevTool``.
|
||||
- ``my_app.tools:Factory.build`` imports ``my_app.tools``, gets ``Factory``,
|
||||
then gets ``build`` from ``Factory``.
|
||||
- ``crewai_tools`` is invalid because it has no ``:``.
|
||||
- ``crewai_tools:`` is invalid because it has no ``qualname``.
|
||||
|
||||
These helpers are the shared contract for YAML/JSON definitions:
|
||||
|
||||
- ``resolve_ref()`` checks the ref, imports the module, and returns the symbol
|
||||
as-is.
|
||||
- ``resolve_class_ref()`` does the same work, then checks that the symbol is a
|
||||
class. It can also check that the class extends a base class. It does not
|
||||
create an object.
|
||||
|
||||
These helpers import user code. Code that must avoid that should check the raw
|
||||
string shape instead.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
from operator import attrgetter
|
||||
from typing import Any
|
||||
|
||||
|
||||
class InvalidRefError(ValueError):
|
||||
"""A definition ref that cannot be resolved to a live Python symbol."""
|
||||
|
||||
|
||||
def resolve_ref(ref: str, *, field: str) -> Any:
|
||||
"""Return the Python symbol named by a project definition field."""
|
||||
module_name, _, qualname = ref.partition(":")
|
||||
if "<" in ref or not module_name or not qualname:
|
||||
raise InvalidRefError(
|
||||
f"invalid {field} ref {ref!r}; expected 'module:qualname'"
|
||||
)
|
||||
try:
|
||||
return attrgetter(qualname)(importlib.import_module(module_name))
|
||||
except (ImportError, AttributeError) as e:
|
||||
raise InvalidRefError(f"unresolvable {field} ref {ref!r}") from e
|
||||
|
||||
|
||||
def resolve_class_ref(
|
||||
ref: str,
|
||||
*,
|
||||
field: str,
|
||||
base_class: type[Any] | None = None,
|
||||
) -> type[Any]:
|
||||
"""Return the named class, with an optional base class check."""
|
||||
target = resolve_ref(ref, field=field)
|
||||
if not inspect.isclass(target):
|
||||
raise InvalidRefError(f"invalid {field} ref {ref!r}; expected a class")
|
||||
if base_class is not None and not issubclass(target, base_class):
|
||||
raise InvalidRefError(
|
||||
f"invalid {field} ref {ref!r}; expected a subclass of "
|
||||
f"{base_class.__module__}.{base_class.__name__}"
|
||||
)
|
||||
return target
|
||||
@@ -7,6 +7,7 @@ flow methods, routing logic, and error handling.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
from types import SimpleNamespace
|
||||
import time
|
||||
from typing import Any
|
||||
@@ -39,8 +40,6 @@ def _build_executor(**kwargs: Any) -> AgentExecutor:
|
||||
executor._human_feedback_method_outputs = {}
|
||||
executor._input_history = []
|
||||
executor._is_execution_resuming = False
|
||||
import threading
|
||||
executor._state_lock = threading.Lock()
|
||||
executor._or_listeners_lock = threading.Lock()
|
||||
executor._execution_lock = threading.Lock()
|
||||
executor._finalize_lock = threading.Lock()
|
||||
|
||||
@@ -385,12 +385,52 @@ class TestLoadAgentFromDefinition:
|
||||
|
||||
|
||||
class TestResolveTools:
|
||||
def test_import_ref_tool_resolves(self, tmp_path, monkeypatch):
|
||||
from crewai.project.json_loader import _resolve_tools
|
||||
|
||||
(tmp_path / "project_tools.py").write_text(
|
||||
"from crewai.tools.base_tool import BaseTool\n"
|
||||
"\n"
|
||||
"class LookupTool(BaseTool):\n"
|
||||
" name: str = 'lookup'\n"
|
||||
" description: str = 'lookup input'\n"
|
||||
"\n"
|
||||
" def _run(self, text: str) -> str:\n"
|
||||
" return text\n"
|
||||
)
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
tools = _resolve_tools(["project_tools:LookupTool"])
|
||||
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "lookup"
|
||||
|
||||
def test_unknown_tool_raises_with_guidance(self):
|
||||
from crewai.project.json_loader import JSONProjectError, _resolve_tools
|
||||
|
||||
with pytest.raises(JSONProjectError, match="Unknown tool 'NotARealToolXYZ'"):
|
||||
_resolve_tools(["NotARealToolXYZ"])
|
||||
|
||||
def test_import_ref_tool_must_resolve_to_basetool_class(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
from crewai.project.json_loader import JSONProjectError, _resolve_tools
|
||||
|
||||
(tmp_path / "not_tools.py").write_text(
|
||||
"class NotATool:\n"
|
||||
" pass\n"
|
||||
)
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
with pytest.raises(JSONProjectError, match="expected a BaseTool class"):
|
||||
_resolve_tools(["not_tools:NotATool"])
|
||||
|
||||
def test_unresolvable_import_ref_tool_raises_guidance(self):
|
||||
from crewai.project.json_loader import JSONProjectError, _resolve_tools
|
||||
|
||||
with pytest.raises(JSONProjectError, match="unresolvable tool ref"):
|
||||
_resolve_tools(["not_a_real_module:MissingTool"])
|
||||
|
||||
def test_missing_custom_tool_raises(self, tmp_path, monkeypatch):
|
||||
from crewai.project.json_loader import JSONProjectError, _resolve_tools
|
||||
|
||||
@@ -505,6 +545,30 @@ class TestValidationDoesNotExecuteTools:
|
||||
|
||||
assert not sentinel.exists(), "validation must not import Python refs"
|
||||
|
||||
def test_validate_does_not_import_tool_refs(
|
||||
self, tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
from crewai.project.json_loader import validate_crew_project
|
||||
|
||||
sentinel = tmp_path / "tool_ref_executed.txt"
|
||||
(tmp_path / "project_tools.py").write_text(
|
||||
"from pathlib import Path\n"
|
||||
f"Path({str(sentinel)!r}).write_text('boom')\n"
|
||||
"from crewai.tools.base_tool import BaseTool\n"
|
||||
"class LookupTool(BaseTool):\n"
|
||||
" name: str = 'lookup'\n"
|
||||
" description: str = 'lookup input'\n"
|
||||
" def _run(self, text: str) -> str:\n"
|
||||
" return text\n"
|
||||
)
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
sys.modules.pop("project_tools", None)
|
||||
crew_path = self._write_project(tmp_path, tool_line='"project_tools:LookupTool"')
|
||||
|
||||
validate_crew_project(crew_path, tmp_path / "agents")
|
||||
|
||||
assert not sentinel.exists(), "validation must not import tool refs"
|
||||
|
||||
def test_validate_reports_missing_custom_tool_file(self, tmp_path):
|
||||
from crewai.project.json_loader import (
|
||||
JSONProjectValidationError,
|
||||
|
||||
@@ -1510,42 +1510,36 @@ def test_conditional_router_events_exclusivity():
|
||||
assert "handle_event_c" not in execution_order
|
||||
|
||||
|
||||
def test_state_consistency_across_parallel_branches():
|
||||
"""Test that state remains consistent when branches execute in parallel.
|
||||
def test_and_join_waits_for_parallel_branches():
|
||||
"""Test that sibling branches complete before a joined listener runs.
|
||||
|
||||
Note: Branches triggered by the same parent execute in parallel for efficiency.
|
||||
Thread-safe state access via StateProxy ensures no race conditions.
|
||||
We check the execution order to ensure the branches execute in parallel.
|
||||
Branches triggered by the same parent execute in parallel for efficiency.
|
||||
Shared state updates are not guaranteed to be atomic, so this test uses a
|
||||
locked local recorder instead of branch state mutation.
|
||||
"""
|
||||
execution_order = []
|
||||
execution_order_lock = threading.Lock()
|
||||
|
||||
def record(method_name: str) -> None:
|
||||
with execution_order_lock:
|
||||
execution_order.append(method_name)
|
||||
|
||||
class StateConsistencyFlow(Flow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.state["counter"] = 0
|
||||
self.state["branch_a_value"] = None
|
||||
self.state["branch_b_value"] = None
|
||||
|
||||
@start()
|
||||
def init(self):
|
||||
execution_order.append("init")
|
||||
self.state["counter"] = 10
|
||||
record("init")
|
||||
|
||||
@listen(init)
|
||||
def branch_a(self):
|
||||
execution_order.append("branch_a")
|
||||
self.state["branch_a_value"] = self.state["counter"]
|
||||
self.state["counter"] += 1
|
||||
record("branch_a")
|
||||
|
||||
@listen(init)
|
||||
def branch_b(self):
|
||||
execution_order.append("branch_b")
|
||||
self.state["branch_b_value"] = self.state["counter"]
|
||||
self.state["counter"] += 5
|
||||
record("branch_b")
|
||||
|
||||
@listen(and_(branch_a, branch_b))
|
||||
def verify_state(self):
|
||||
execution_order.append("verify_state")
|
||||
record("verify_state")
|
||||
|
||||
flow = StateConsistencyFlow()
|
||||
flow.kickoff()
|
||||
@@ -1554,10 +1548,8 @@ def test_state_consistency_across_parallel_branches():
|
||||
assert "branch_b" in execution_order
|
||||
assert "verify_state" in execution_order
|
||||
|
||||
assert flow.state["branch_a_value"] is not None
|
||||
assert flow.state["branch_b_value"] is not None
|
||||
|
||||
assert flow.state["counter"] == 16
|
||||
assert execution_order.index("branch_a") < execution_order.index("verify_state")
|
||||
assert execution_order.index("branch_b") < execution_order.index("verify_state")
|
||||
|
||||
|
||||
def test_deeply_nested_conditions():
|
||||
|
||||
@@ -928,8 +928,6 @@ class TestConversationalFlow:
|
||||
conversational = True
|
||||
|
||||
flow = BareChat()
|
||||
# ``flow.state`` returns a ``StateProxy``; the underlying state is
|
||||
# on ``flow._state``. Both views expose the same chat-shaped fields.
|
||||
assert isinstance(flow._state, ConversationState)
|
||||
assert flow.state.messages == []
|
||||
assert flow.state.current_user_message is None
|
||||
|
||||
@@ -357,6 +357,27 @@ methods:
|
||||
listen: begin
|
||||
"""
|
||||
|
||||
JSON_SCHEMA_REQUIRED_INPUT_STATE_YAML = """
|
||||
schema: crewai.flow/v1
|
||||
name: JsonSchemaRequiredInputStateFlow
|
||||
state:
|
||||
type: json_schema
|
||||
json_schema:
|
||||
title: LeadState
|
||||
type: object
|
||||
required:
|
||||
- lead_name
|
||||
properties:
|
||||
lead_name:
|
||||
type: string
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: state.lead_name
|
||||
"""
|
||||
|
||||
PYDANTIC_REF_WITH_SCHEMA_FALLBACK_YAML = f"""
|
||||
schema: crewai.flow/v1
|
||||
name: SchemaFallbackFlow
|
||||
@@ -445,7 +466,8 @@ def _run_with_events(flow, inputs=None):
|
||||
|
||||
|
||||
def _state_without_id(flow):
|
||||
snapshot = dict(flow.state.model_dump())
|
||||
state = flow.state
|
||||
snapshot = dict(state if isinstance(state, dict) else state.model_dump())
|
||||
snapshot.pop("id", None)
|
||||
return snapshot
|
||||
|
||||
@@ -2298,6 +2320,18 @@ def test_json_schema_state_validates_inputs():
|
||||
flow.kickoff(inputs={"count": "not-a-number"})
|
||||
|
||||
|
||||
def test_json_schema_state_required_fields_can_come_from_kickoff_inputs():
|
||||
flow = Flow.from_definition(
|
||||
FlowDefinition.from_declaration(contents=JSON_SCHEMA_REQUIRED_INPUT_STATE_YAML)
|
||||
)
|
||||
|
||||
result = flow.kickoff(inputs={"lead_name": "Ada Lovelace"})
|
||||
|
||||
assert result == "Ada Lovelace"
|
||||
assert flow.state.lead_name == "Ada Lovelace"
|
||||
assert flow.state.id
|
||||
|
||||
|
||||
def test_pydantic_state_falls_back_to_json_schema_when_ref_unimportable():
|
||||
flow = Flow.from_definition(
|
||||
FlowDefinition.from_declaration(contents=PYDANTIC_REF_WITH_SCHEMA_FALLBACK_YAML)
|
||||
|
||||
@@ -233,7 +233,7 @@ def test_persistence_with_base_model(tmp_path):
|
||||
assert message.role == "user"
|
||||
assert message.type == "text"
|
||||
assert message.content == "Hello, World!"
|
||||
assert isinstance(flow.state._unwrap(), State)
|
||||
assert isinstance(flow.state, State)
|
||||
|
||||
|
||||
def test_fork_with_restore_from_state_id(tmp_path):
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a5"
|
||||
|
||||
Reference in New Issue
Block a user