mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-06-24 17:48:11 +00:00
Compare commits
6 Commits
1.14.8a4
...
docs/cor-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d70afa6f | ||
|
|
db35033294 | ||
|
|
1fa3b75425 | ||
|
|
f4e4662421 | ||
|
|
cb18653c8b | ||
|
|
3f5ca89edc |
@@ -551,6 +551,35 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Platform API",
|
||||
"icon": "code",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": [
|
||||
"edge/api/v1/platform-api/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"openapi": {
|
||||
"source": "/edge/openapi/platform-v1.yaml",
|
||||
"directory": "edge/api/v1/platform-api/reference"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Problems",
|
||||
"pages": [
|
||||
"edge/api/v1/problems",
|
||||
"edge/api/v1/problems/bad_request",
|
||||
"edge/api/v1/problems/not_found",
|
||||
"edge/api/v1/problems/validation_error",
|
||||
"edge/api/v1/problems/internal_error"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Examples",
|
||||
"icon": "code",
|
||||
@@ -1075,6 +1104,35 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Platform API",
|
||||
"icon": "code",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": [
|
||||
"v1.14.7/api/v1/platform-api/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"openapi": {
|
||||
"source": "/v1.14.7/openapi/platform-v1.yaml",
|
||||
"directory": "v1.14.7/api/v1/platform-api/reference"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Problems",
|
||||
"pages": [
|
||||
"v1.14.7/api/v1/problems",
|
||||
"v1.14.7/api/v1/problems/bad_request",
|
||||
"v1.14.7/api/v1/problems/not_found",
|
||||
"v1.14.7/api/v1/problems/validation_error",
|
||||
"v1.14.7/api/v1/problems/internal_error"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Examples",
|
||||
"icon": "code",
|
||||
|
||||
19
docs/edge/api/v1/platform-api/introduction.mdx
Normal file
19
docs/edge/api/v1/platform-api/introduction.mdx
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: "Platform API"
|
||||
description: "Build against the supported CrewAI Platform public API."
|
||||
icon: "code"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# CrewAI Platform API
|
||||
|
||||
The Platform API is the supported public API for CrewAI Platform. Use it when
|
||||
you need stable HTTP contracts for automation, integrations, and agent-facing
|
||||
workflows.
|
||||
|
||||
The current public contract is `v1`. Endpoints live under `/api/v1` on the
|
||||
CrewAI Platform app host:
|
||||
|
||||
```text
|
||||
https://app.crewai.com/api/v1
|
||||
```
|
||||
13
docs/edge/api/v1/problems.mdx
Normal file
13
docs/edge/api/v1/problems.mdx
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: "Problems"
|
||||
description: "Error responses returned by the CrewAI Platform API."
|
||||
---
|
||||
|
||||
# Problems
|
||||
|
||||
When a request fails, the Platform API returns an `errors` array. Each item
|
||||
includes a stable `code`, an HTTP `status`, and a `detail` message with
|
||||
request-specific context.
|
||||
|
||||
Use the pages in this section to understand what each error code means and what
|
||||
to change before retrying.
|
||||
17
docs/edge/api/v1/problems/bad_request.mdx
Normal file
17
docs/edge/api/v1/problems/bad_request.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
code: bad_request
|
||||
title: Bad request
|
||||
status: 400
|
||||
---
|
||||
|
||||
# Bad request
|
||||
|
||||
The request could not be processed because it was malformed or missing required request data.
|
||||
|
||||
## When It Happens
|
||||
|
||||
This usually means the request body, query string, headers, or required parameters are invalid before endpoint-specific validation can run.
|
||||
|
||||
## How To Fix
|
||||
|
||||
Review the endpoint contract, required parameters, request body shape, and content type before retrying.
|
||||
17
docs/edge/api/v1/problems/internal_error.mdx
Normal file
17
docs/edge/api/v1/problems/internal_error.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
code: internal_error
|
||||
title: Internal error
|
||||
status: 500
|
||||
---
|
||||
|
||||
# Internal error
|
||||
|
||||
An unexpected server-side failure prevented the request from completing.
|
||||
|
||||
## When It Happens
|
||||
|
||||
This means the platform encountered an unexpected condition while processing a valid request.
|
||||
|
||||
## How To Fix
|
||||
|
||||
Retry the request after a short delay. If the problem continues, contact support with the request details and timestamp.
|
||||
17
docs/edge/api/v1/problems/not_found.mdx
Normal file
17
docs/edge/api/v1/problems/not_found.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
code: not_found
|
||||
title: Not found
|
||||
status: 404
|
||||
---
|
||||
|
||||
# Not found
|
||||
|
||||
The requested resource does not exist or is not available at the requested path.
|
||||
|
||||
## When It Happens
|
||||
|
||||
This can happen when the URL is incorrect, the resource identifier does not exist, or the resource is not visible through the public API.
|
||||
|
||||
## How To Fix
|
||||
|
||||
Check the endpoint path and resource identifier, then retry with a resource that exists and is available to the request.
|
||||
17
docs/edge/api/v1/problems/validation_error.mdx
Normal file
17
docs/edge/api/v1/problems/validation_error.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
code: validation_error
|
||||
title: Validation error
|
||||
status: 422
|
||||
---
|
||||
|
||||
# Validation error
|
||||
|
||||
The request was understood, but one or more submitted values failed validation.
|
||||
|
||||
## When It Happens
|
||||
|
||||
This usually means a submitted field is missing, malformed, out of range, duplicated, or conflicts with another value.
|
||||
|
||||
## How To Fix
|
||||
|
||||
Inspect the `detail` message for the field-specific issue, update the submitted values, and retry the request.
|
||||
@@ -4,57 +4,6 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="24 يونيو 2026">
|
||||
## v1.14.8a4
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a4)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- دعم تدفقات المحادثة في واجهة سطر الأوامر TUI.
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح مسار التوجيه الرمزي في استخراج أرشيف المهارات.
|
||||
- التحقق من صحة مسارات تعريف التدفق الإعلاني.
|
||||
|
||||
### الوثائق
|
||||
- تحديث اللقطة وسجل التغييرات للإصدار v1.14.8a3.
|
||||
|
||||
## المساهمون
|
||||
|
||||
@lorenzejay, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="23 يونيو 2026">
|
||||
## v1.14.8a3
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a3)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة تحميل تدفق موحد إعلاني
|
||||
- تحسين تجربة بدء تشغيل crewai run
|
||||
- دمج `crewai run` و `crewai flow kickoff`
|
||||
- الحفاظ على تقدم طريقة التدفق مرئيًا للفرق المتداخلة
|
||||
- إضافة دعم واجهة سطر الأوامر الإعلانية للتدفق
|
||||
- السماح باستخدام `@router()` كطريقة بدء لتدفق
|
||||
- إضافة مخططات مخرجات مكتوبة لأدوات CrewAI
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- تثبيت opentelemetry على ~=1.42.0
|
||||
|
||||
### الوثائق
|
||||
- إضافة صفحة استوديو "بطاقة واحدة لكل خطوة"
|
||||
|
||||
## المساهمون
|
||||
|
||||
@jessemiller, @joaomdmoura, @lucasgomide, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="18 يونيو 2026">
|
||||
## v1.14.8a2
|
||||
|
||||
|
||||
@@ -4,57 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Jun 24, 2026">
|
||||
## v1.14.8a4
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a4)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Support conversational flows in the CLI TUI.
|
||||
|
||||
### Bug Fixes
|
||||
- Fix symlink path traversal in skill archive extraction.
|
||||
- Validate declarative flow definition paths.
|
||||
|
||||
### Documentation
|
||||
- Update snapshot and changelog for v1.14.8a3.
|
||||
|
||||
## Contributors
|
||||
|
||||
@lorenzejay, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Jun 23, 2026">
|
||||
## v1.14.8a3
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a3)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add unified declarative flow loading
|
||||
- Improve crewai run startup UX
|
||||
- Consolidate `crewai run` and `crewai flow kickoff`
|
||||
- Keep flow method progress visible for nested crews
|
||||
- Add declarative Flow CLI support
|
||||
- Allow `@router()` as start method of a flow
|
||||
- Add typed output schemas for CrewAI tools
|
||||
|
||||
### Bug Fixes
|
||||
- Pin opentelemetry to ~=1.42.0
|
||||
|
||||
### Documentation
|
||||
- Add "One Card per Step" Studio page
|
||||
|
||||
## Contributors
|
||||
|
||||
@jessemiller, @joaomdmoura, @lucasgomide, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Jun 18, 2026">
|
||||
## v1.14.8a2
|
||||
|
||||
|
||||
@@ -4,57 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 6월 24일">
|
||||
## v1.14.8a4
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a4)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- CLI TUI에서 대화형 흐름 지원.
|
||||
|
||||
### 버그 수정
|
||||
- 기술 아카이브 추출 시 심볼릭 링크 경로 탐색 문제 수정.
|
||||
- 선언적 흐름 정의 경로 검증.
|
||||
|
||||
### 문서
|
||||
- v1.14.8a3에 대한 스냅샷 및 변경 로그 업데이트.
|
||||
|
||||
## 기여자
|
||||
|
||||
@lorenzejay, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 6월 23일">
|
||||
## v1.14.8a3
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a3)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 통합 선언적 흐름 로딩 추가
|
||||
- crewai run 시작 UX 개선
|
||||
- `crewai run`과 `crewai flow kickoff` 통합
|
||||
- 중첩된 크루의 흐름 메서드 진행 상황 표시 유지
|
||||
- 선언적 Flow CLI 지원 추가
|
||||
- 흐름의 시작 메서드로 `@router()` 허용
|
||||
- CrewAI 도구에 대한 타입이 지정된 출력 스키마 추가
|
||||
|
||||
### 버그 수정
|
||||
- opentelemetry를 ~=1.42.0으로 고정
|
||||
|
||||
### 문서
|
||||
- "단계당 한 카드" 스튜디오 페이지 추가
|
||||
|
||||
## 기여자
|
||||
|
||||
@jessemiller, @joaomdmoura, @lucasgomide, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 6월 18일">
|
||||
## v1.14.8a2
|
||||
|
||||
|
||||
115
docs/edge/openapi/platform-v1.yaml
Normal file
115
docs/edge/openapi/platform-v1.yaml
Normal file
@@ -0,0 +1,115 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: CrewAI Platform API
|
||||
version: v1
|
||||
description: Supported public API for CrewAI Platform.
|
||||
servers:
|
||||
- url: https://app.crewai.com
|
||||
description: CrewAI Platform
|
||||
security: []
|
||||
tags:
|
||||
- name: Status
|
||||
description: Platform health and API availability.
|
||||
paths:
|
||||
/api/v1/status:
|
||||
get:
|
||||
summary: Check API status
|
||||
operationId: getStatus
|
||||
tags:
|
||||
- Status
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
success:
|
||||
value:
|
||||
data:
|
||||
status: ok
|
||||
summary: API is available
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
additionalProperties: false
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- ok
|
||||
example: ok
|
||||
components:
|
||||
schemas:
|
||||
SuccessEnvelope:
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
description: Endpoint-specific response payload.
|
||||
ErrorEnvelope:
|
||||
type: object
|
||||
required:
|
||||
- errors
|
||||
additionalProperties: false
|
||||
properties:
|
||||
errors:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: '#/components/schemas/Error'
|
||||
Error:
|
||||
type: object
|
||||
description: Public API error object.
|
||||
required:
|
||||
- type
|
||||
- code
|
||||
- title
|
||||
- status
|
||||
- detail
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
format: uri
|
||||
enum:
|
||||
- https://docs.crewai.com/api/v1/problems/bad_request
|
||||
- https://docs.crewai.com/api/v1/problems/internal_error
|
||||
- https://docs.crewai.com/api/v1/problems/not_found
|
||||
- https://docs.crewai.com/api/v1/problems/validation_error
|
||||
example: https://docs.crewai.com/api/v1/problems/bad_request
|
||||
code:
|
||||
type: string
|
||||
enum:
|
||||
- bad_request
|
||||
- internal_error
|
||||
- not_found
|
||||
- validation_error
|
||||
example: bad_request
|
||||
title:
|
||||
type: string
|
||||
enum:
|
||||
- Bad request
|
||||
- Internal error
|
||||
- Not found
|
||||
- Validation error
|
||||
example: Bad request
|
||||
status:
|
||||
type: integer
|
||||
enum:
|
||||
- 400
|
||||
- 404
|
||||
- 422
|
||||
- 500
|
||||
example: 400
|
||||
detail:
|
||||
type: string
|
||||
example: The request is invalid.
|
||||
@@ -4,57 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="24 jun 2026">
|
||||
## v1.14.8a4
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a4)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Suporte a fluxos de conversa na TUI do CLI.
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir a travessia de caminho de symlink na extração de arquivo de habilidade.
|
||||
- Validar os caminhos de definição de fluxo declarativo.
|
||||
|
||||
### Documentação
|
||||
- Atualizar snapshot e changelog para v1.14.8a3.
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@lorenzejay, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="23 jun 2026">
|
||||
## v1.14.8a3
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a3)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar carregamento de fluxo declarativo unificado
|
||||
- Melhorar a experiência de inicialização do crewai run
|
||||
- Consolidar `crewai run` e `crewai flow kickoff`
|
||||
- Manter o progresso do método de fluxo visível para equipes aninhadas
|
||||
- Adicionar suporte a Flow CLI declarativo
|
||||
- Permitir `@router()` como método de início de um fluxo
|
||||
- Adicionar esquemas de saída tipados para ferramentas CrewAI
|
||||
|
||||
### Correções de Bugs
|
||||
- Fixar opentelemetry em ~=1.42.0
|
||||
|
||||
### Documentação
|
||||
- Adicionar página "Uma Cartão por Etapa" no Studio
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@jessemiller, @joaomdmoura, @lucasgomide, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="18 jun 2026">
|
||||
## v1.14.8a2
|
||||
|
||||
|
||||
19
docs/v1.14.7/api/v1/platform-api/introduction.mdx
Normal file
19
docs/v1.14.7/api/v1/platform-api/introduction.mdx
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: "Platform API"
|
||||
description: "Build against the supported CrewAI Platform public API."
|
||||
icon: "code"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# CrewAI Platform API
|
||||
|
||||
The Platform API is the supported public API for CrewAI Platform. Use it when
|
||||
you need stable HTTP contracts for automation, integrations, and agent-facing
|
||||
workflows.
|
||||
|
||||
The current public contract is `v1`. Endpoints live under `/api/v1` on the
|
||||
CrewAI Platform app host:
|
||||
|
||||
```text
|
||||
https://app.crewai.com/api/v1
|
||||
```
|
||||
13
docs/v1.14.7/api/v1/problems.mdx
Normal file
13
docs/v1.14.7/api/v1/problems.mdx
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: "Problems"
|
||||
description: "Error responses returned by the CrewAI Platform API."
|
||||
---
|
||||
|
||||
# Problems
|
||||
|
||||
When a request fails, the Platform API returns an `errors` array. Each item
|
||||
includes a stable `code`, an HTTP `status`, and a `detail` message with
|
||||
request-specific context.
|
||||
|
||||
Use the pages in this section to understand what each error code means and what
|
||||
to change before retrying.
|
||||
17
docs/v1.14.7/api/v1/problems/bad_request.mdx
Normal file
17
docs/v1.14.7/api/v1/problems/bad_request.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
code: bad_request
|
||||
title: Bad request
|
||||
status: 400
|
||||
---
|
||||
|
||||
# Bad request
|
||||
|
||||
The request could not be processed because it was malformed or missing required request data.
|
||||
|
||||
## When It Happens
|
||||
|
||||
This usually means the request body, query string, headers, or required parameters are invalid before endpoint-specific validation can run.
|
||||
|
||||
## How To Fix
|
||||
|
||||
Review the endpoint contract, required parameters, request body shape, and content type before retrying.
|
||||
17
docs/v1.14.7/api/v1/problems/internal_error.mdx
Normal file
17
docs/v1.14.7/api/v1/problems/internal_error.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
code: internal_error
|
||||
title: Internal error
|
||||
status: 500
|
||||
---
|
||||
|
||||
# Internal error
|
||||
|
||||
An unexpected server-side failure prevented the request from completing.
|
||||
|
||||
## When It Happens
|
||||
|
||||
This means the platform encountered an unexpected condition while processing a valid request.
|
||||
|
||||
## How To Fix
|
||||
|
||||
Retry the request after a short delay. If the problem continues, contact support with the request details and timestamp.
|
||||
17
docs/v1.14.7/api/v1/problems/not_found.mdx
Normal file
17
docs/v1.14.7/api/v1/problems/not_found.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
code: not_found
|
||||
title: Not found
|
||||
status: 404
|
||||
---
|
||||
|
||||
# Not found
|
||||
|
||||
The requested resource does not exist or is not available at the requested path.
|
||||
|
||||
## When It Happens
|
||||
|
||||
This can happen when the URL is incorrect, the resource identifier does not exist, or the resource is not visible through the public API.
|
||||
|
||||
## How To Fix
|
||||
|
||||
Check the endpoint path and resource identifier, then retry with a resource that exists and is available to the request.
|
||||
17
docs/v1.14.7/api/v1/problems/validation_error.mdx
Normal file
17
docs/v1.14.7/api/v1/problems/validation_error.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
code: validation_error
|
||||
title: Validation error
|
||||
status: 422
|
||||
---
|
||||
|
||||
# Validation error
|
||||
|
||||
The request was understood, but one or more submitted values failed validation.
|
||||
|
||||
## When It Happens
|
||||
|
||||
This usually means a submitted field is missing, malformed, out of range, duplicated, or conflicts with another value.
|
||||
|
||||
## How To Fix
|
||||
|
||||
Inspect the `detail` message for the field-specific issue, update the submitted values, and retry the request.
|
||||
115
docs/v1.14.7/openapi/platform-v1.yaml
Normal file
115
docs/v1.14.7/openapi/platform-v1.yaml
Normal file
@@ -0,0 +1,115 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: CrewAI Platform API
|
||||
version: v1
|
||||
description: Supported public API for CrewAI Platform.
|
||||
servers:
|
||||
- url: https://app.crewai.com
|
||||
description: CrewAI Platform
|
||||
security: []
|
||||
tags:
|
||||
- name: Status
|
||||
description: Platform health and API availability.
|
||||
paths:
|
||||
/api/v1/status:
|
||||
get:
|
||||
summary: Check API status
|
||||
operationId: getStatus
|
||||
tags:
|
||||
- Status
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
success:
|
||||
value:
|
||||
data:
|
||||
status: ok
|
||||
summary: API is available
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
additionalProperties: false
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- ok
|
||||
example: ok
|
||||
components:
|
||||
schemas:
|
||||
SuccessEnvelope:
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
description: Endpoint-specific response payload.
|
||||
ErrorEnvelope:
|
||||
type: object
|
||||
required:
|
||||
- errors
|
||||
additionalProperties: false
|
||||
properties:
|
||||
errors:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: '#/components/schemas/Error'
|
||||
Error:
|
||||
type: object
|
||||
description: Public API error object.
|
||||
required:
|
||||
- type
|
||||
- code
|
||||
- title
|
||||
- status
|
||||
- detail
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
format: uri
|
||||
enum:
|
||||
- https://docs.crewai.com/api/v1/problems/bad_request
|
||||
- https://docs.crewai.com/api/v1/problems/internal_error
|
||||
- https://docs.crewai.com/api/v1/problems/not_found
|
||||
- https://docs.crewai.com/api/v1/problems/validation_error
|
||||
example: https://docs.crewai.com/api/v1/problems/bad_request
|
||||
code:
|
||||
type: string
|
||||
enum:
|
||||
- bad_request
|
||||
- internal_error
|
||||
- not_found
|
||||
- validation_error
|
||||
example: bad_request
|
||||
title:
|
||||
type: string
|
||||
enum:
|
||||
- Bad request
|
||||
- Internal error
|
||||
- Not found
|
||||
- Validation error
|
||||
example: Bad request
|
||||
status:
|
||||
type: integer
|
||||
enum:
|
||||
- 400
|
||||
- 404
|
||||
- 422
|
||||
- 500
|
||||
example: 400
|
||||
detail:
|
||||
type: string
|
||||
example: The request is invalid.
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.8a4",
|
||||
"crewai-core==1.14.8a2",
|
||||
"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.8a2"
|
||||
|
||||
@@ -17,7 +17,7 @@ from textual.binding import Binding, BindingType
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
from textual.css.query import NoMatches
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Footer, Header, Input, Static
|
||||
from textual.widgets import Button, Footer, Header, Static
|
||||
|
||||
|
||||
_SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
@@ -382,18 +382,6 @@ Screen {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#conversation-input {
|
||||
display: none;
|
||||
height: 3;
|
||||
border-top: hkey #333333;
|
||||
background: #1c1c1c;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#conversation-input:focus {
|
||||
border-top: hkey #1F7982;
|
||||
}
|
||||
|
||||
Header {
|
||||
background: #1c1c1c;
|
||||
color: #FF5A50;
|
||||
@@ -495,7 +483,6 @@ FooterKey .footer-key--key {
|
||||
total_tasks: int = 0,
|
||||
agent_names: list[str] | None = None,
|
||||
task_names: list[str] | None = None,
|
||||
conversational: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self.title = f"CrewAI — {crew_name}"
|
||||
@@ -557,13 +544,6 @@ FooterKey .footer-key--key {
|
||||
self._event_handlers: list[tuple[type, Any]] = []
|
||||
|
||||
self._crew: Any = None
|
||||
self._flow: Any = None
|
||||
self._is_conversational = conversational
|
||||
self._conversation_messages: list[tuple[str, str]] = []
|
||||
self._conversation_turns = 0
|
||||
self._conversation_turn_in_progress = False
|
||||
self._conversation_previous_defer_trace_finalization: bool | None = None
|
||||
self._conversation_exit_commands = {"exit", "quit"}
|
||||
self._default_inputs: dict[str, Any] | None = None
|
||||
self._crew_result: Any = None
|
||||
self._crew_json_path: Any = None
|
||||
@@ -586,10 +566,6 @@ FooterKey .footer-key--key {
|
||||
yield Static(id="task-header")
|
||||
with VerticalScroll(id="scroll-area"):
|
||||
yield Static(id="main-content")
|
||||
yield Input(
|
||||
placeholder="Message the flow...",
|
||||
id="conversation-input",
|
||||
)
|
||||
with VerticalScroll(id="log-panel"):
|
||||
yield Static(id="log-content")
|
||||
yield Footer()
|
||||
@@ -598,9 +574,7 @@ FooterKey .footer-key--key {
|
||||
self._start_time = time.time()
|
||||
self._subscribe()
|
||||
self._tick_timer = self.set_interval(1 / 8, self._tick)
|
||||
if self._is_conversational and self._flow:
|
||||
self._start_conversational_session()
|
||||
elif self._crew:
|
||||
if self._crew:
|
||||
self._run_crew_worker()
|
||||
elif self._crew_json_path:
|
||||
self._load_and_run_worker()
|
||||
@@ -751,140 +725,6 @@ FooterKey .footer-key--key {
|
||||
self._tick_timer = self.set_interval(1 / 2, self._tick)
|
||||
self._unsubscribe_if_no_running_memory_save(wait_for_queued=True)
|
||||
|
||||
# ── Conversational flow execution ───────────────────────
|
||||
|
||||
def _start_conversational_session(self) -> None:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
set_suppress_tracing_messages,
|
||||
set_tui_mode,
|
||||
)
|
||||
|
||||
set_tui_mode(True)
|
||||
set_suppress_tracing_messages(True)
|
||||
with self._lock:
|
||||
self._status = "chatting"
|
||||
self._current_step = None
|
||||
self._elapsed_frozen = None
|
||||
self._conversation_previous_defer_trace_finalization = getattr(
|
||||
self._flow, "defer_trace_finalization", False
|
||||
)
|
||||
self._flow.defer_trace_finalization = True
|
||||
|
||||
try:
|
||||
input_widget = self.query_one("#conversation-input", Input)
|
||||
input_widget.display = True
|
||||
input_widget.focus()
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def _finalize_conversational_session(self) -> None:
|
||||
if not (self._is_conversational and self._flow):
|
||||
return
|
||||
try:
|
||||
self._flow.finalize_session_traces()
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
previous = self._conversation_previous_defer_trace_finalization
|
||||
if previous is not None:
|
||||
try:
|
||||
self._flow.defer_trace_finalization = previous
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
if event.input.id != "conversation-input":
|
||||
return
|
||||
if not self._is_conversational:
|
||||
return
|
||||
|
||||
message = event.value.strip()
|
||||
event.input.value = ""
|
||||
if not message:
|
||||
return
|
||||
if message.lower() in self._conversation_exit_commands:
|
||||
self._finalize_conversational_session()
|
||||
self._unsubscribe()
|
||||
self.exit(self._crew_result)
|
||||
return
|
||||
if self._conversation_turn_in_progress:
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
self._conversation_messages.append(("user", message))
|
||||
self._conversation_turn_in_progress = True
|
||||
self._conversation_turns += 1
|
||||
self._status = "working"
|
||||
self._current_step = ("yellow", "Thinking…", "")
|
||||
self._is_streaming = False
|
||||
self._streaming_text = ""
|
||||
self._task_full_output = ""
|
||||
self._current_llm_text = ""
|
||||
|
||||
event.input.disabled = True
|
||||
self._run_conversation_turn_worker(message)
|
||||
|
||||
@work(thread=True, exclusive=True, group="conversation")
|
||||
def _run_conversation_turn_worker(self, message: str) -> None:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
set_suppress_tracing_messages,
|
||||
set_tui_mode,
|
||||
)
|
||||
|
||||
set_tui_mode(True)
|
||||
set_suppress_tracing_messages(True)
|
||||
try:
|
||||
result = self._flow.handle_turn(message)
|
||||
if hasattr(result, "get_full_text") and hasattr(result, "result"):
|
||||
for _chunk in result:
|
||||
pass
|
||||
result = result.result
|
||||
self.call_from_thread(self._on_conversation_turn_done, result)
|
||||
except Exception as e:
|
||||
self.call_from_thread(self._on_conversation_turn_failed, str(e))
|
||||
|
||||
def _on_conversation_turn_done(self, result: Any) -> None:
|
||||
with self._lock:
|
||||
output = self._stringify_output(result)
|
||||
self._conversation_messages.append(("assistant", output))
|
||||
self._crew_result = result
|
||||
self._conversation_turn_in_progress = False
|
||||
self._status = "chatting"
|
||||
self._is_streaming = False
|
||||
self._streaming_text = ""
|
||||
self._current_step = None
|
||||
self._enable_conversation_input()
|
||||
self._tick()
|
||||
self._scroll_to_result()
|
||||
|
||||
def _on_conversation_turn_failed(self, error: str) -> None:
|
||||
with self._lock:
|
||||
self._status = "failed"
|
||||
self._error = error
|
||||
self._conversation_turn_in_progress = False
|
||||
self._is_streaming = False
|
||||
self._current_step = None
|
||||
self._enable_conversation_input()
|
||||
self._tick()
|
||||
|
||||
def _enable_conversation_input(self) -> None:
|
||||
try:
|
||||
input_widget = self.query_one("#conversation-input", Input)
|
||||
input_widget.disabled = False
|
||||
input_widget.focus()
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def _stringify_output(self, result: Any) -> str:
|
||||
raw_result = getattr(result, "raw", result)
|
||||
if raw_result is None:
|
||||
return ""
|
||||
if isinstance(raw_result, str):
|
||||
return raw_result
|
||||
try:
|
||||
return _json.dumps(raw_result, default=str, ensure_ascii=False)
|
||||
except TypeError:
|
||||
return str(raw_result)
|
||||
|
||||
# ── Actions ─────────────────────────────────────────────
|
||||
|
||||
def action_toggle_sidebar(self) -> None:
|
||||
@@ -943,7 +783,6 @@ FooterKey .footer-key--key {
|
||||
self._refresh_log_panel()
|
||||
|
||||
async def action_quit(self) -> None:
|
||||
self._finalize_conversational_session()
|
||||
self._unsubscribe()
|
||||
self.exit(self._crew_result)
|
||||
|
||||
@@ -1119,30 +958,6 @@ FooterKey .footer-key--key {
|
||||
t = Text()
|
||||
sidebar_width = 30
|
||||
|
||||
if self._is_conversational:
|
||||
t.append(" CONVERSATION\n", style=f"bold {_C_PRIMARY}")
|
||||
t.append("\n")
|
||||
if self._conversation_turn_in_progress:
|
||||
t.append(f" {self._spinner()} ", style=_C_PRIMARY)
|
||||
t.append("Working\n", style=f"bold {_C_TEXT}")
|
||||
elif self._status == "failed":
|
||||
t.append(" ✘ Failed\n", style=_C_RED)
|
||||
else:
|
||||
t.append(" ● Ready\n", style=_C_GREEN)
|
||||
t.append(f" Turns {self._conversation_turns}\n", style=_C_DIM)
|
||||
t.append("\n")
|
||||
t.append(" TOKENS\n", style=f"bold {_C_PRIMARY}")
|
||||
t.append("\n")
|
||||
out = self._output_tokens + self._live_out_tokens
|
||||
t.append(f" ↑ {self._input_tokens:,}\n", style=_C_DIM)
|
||||
t.append(f" ↓ {out:,}\n", style=_C_DIM)
|
||||
t.append("\n")
|
||||
t.append(" COMMANDS\n", style=f"bold {_C_PRIMARY}")
|
||||
t.append("\n")
|
||||
t.append(" quit / exit\n", style=_C_DIM)
|
||||
widget.update(t)
|
||||
return
|
||||
|
||||
t.append(" TASKS\n", style=f"bold {_C_PRIMARY}")
|
||||
t.append("\n")
|
||||
|
||||
@@ -1196,22 +1011,6 @@ FooterKey .footer-key--key {
|
||||
widget = self.query_one("#task-header", Static)
|
||||
t = Text()
|
||||
|
||||
if self._is_conversational:
|
||||
if self._status == "failed":
|
||||
t.append("✘ ", style=f"bold {_C_RED}")
|
||||
t.append("Failed", style=f"bold {_C_RED}")
|
||||
if self._error:
|
||||
t.append(f"\n{self._error[:120]}", style=_C_RED)
|
||||
elif self._conversation_turn_in_progress:
|
||||
t.append(f"{self._spinner()} ", style=_C_PRIMARY)
|
||||
t.append("Flow is responding", style=f"bold {_C_PRIMARY}")
|
||||
else:
|
||||
t.append("● ", style=f"bold {_C_GREEN}")
|
||||
t.append("Conversational flow ready", style=f"bold {_C_GREEN}")
|
||||
t.append(" Type a message below", style=_C_DIM)
|
||||
widget.update(t)
|
||||
return
|
||||
|
||||
if self._status == "completed":
|
||||
elapsed = self._elapsed_frozen or (time.time() - self._start_time)
|
||||
t.append("✔ ", style=f"bold {_C_GREEN}")
|
||||
@@ -1263,41 +1062,6 @@ FooterKey .footer-key--key {
|
||||
t = Text()
|
||||
should_scroll = False
|
||||
|
||||
if self._is_conversational:
|
||||
if not self._conversation_messages and not self._is_streaming:
|
||||
t.append(" Start the conversation below.\n", style=_C_MUTED)
|
||||
for role, content in self._conversation_messages:
|
||||
if role == "user":
|
||||
t.append("\n You\n", style=f"bold {_C_TEAL}")
|
||||
else:
|
||||
t.append("\n Assistant\n", style=f"bold {_C_PRIMARY}")
|
||||
rendered = _format_json_in_text(_unescape_text(content))
|
||||
for line in rendered.split("\n"):
|
||||
style = _C_TEXT if role == "assistant" else _C_DIM
|
||||
t.append(f" {line}\n", style=style)
|
||||
|
||||
if self._is_streaming and self._streaming_text:
|
||||
text = _unescape_text(self._filtered_streaming_text())
|
||||
if text.strip():
|
||||
t.append("\n Assistant\n", style=f"bold {_C_PRIMARY}")
|
||||
for line in text.rstrip().split("\n")[-40:]:
|
||||
t.append(f" {line}\n", style=_C_TEXT)
|
||||
should_scroll = True
|
||||
|
||||
if self._status == "failed" and self._error:
|
||||
t.append("\n Error\n", style=f"bold {_C_RED}")
|
||||
t.append(f" {self._error}\n", style=_C_RED)
|
||||
|
||||
widget.update(t)
|
||||
if should_scroll:
|
||||
try:
|
||||
self.query_one("#scroll-area", VerticalScroll).scroll_end(
|
||||
animate=False
|
||||
)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
return
|
||||
|
||||
# Plan section
|
||||
if self._plan and self._plan.get("steps"):
|
||||
plan_title = self._plan.get("plan", "Plan")
|
||||
|
||||
@@ -378,40 +378,12 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
|
||||
|
||||
|
||||
def _safe_extractall(tf: tarfile.TarFile, dest: Path) -> None:
|
||||
"""Path-traversal-safe extraction for Python versions without tar filters.
|
||||
|
||||
Validates both the member's own path and, for symlink/hardlink members,
|
||||
the link target. Without the link-target check a malicious archive can
|
||||
plant a symlink that escapes ``dest`` (e.g. ``link -> /home/user/.ssh``)
|
||||
followed by a regular member written *through* that link
|
||||
(``link/authorized_keys``), escaping ``dest`` even though every member
|
||||
name resolves inside it. This mirrors the protection that
|
||||
``tarfile.extractall(..., filter="data")`` provides when available.
|
||||
"""
|
||||
"""Path-traversal-safe extraction for Python < 3.12."""
|
||||
dest_resolved = dest.resolve()
|
||||
for member in tf.getmembers():
|
||||
member_path = (dest / member.name).resolve()
|
||||
if not member_path.is_relative_to(dest_resolved):
|
||||
raise ValueError(f"Blocked path traversal attempt: {member.name!r}")
|
||||
if not (member.isfile() or member.isdir() or member.issym() or member.islnk()):
|
||||
raise ValueError(f"Blocked unsupported tar member: {member.name!r}")
|
||||
if member.issym() or member.islnk():
|
||||
link_target = member.linkname
|
||||
# Absolute link targets always escape the destination.
|
||||
if os.path.isabs(link_target):
|
||||
raise ValueError(
|
||||
f"Blocked link target escaping destination: "
|
||||
f"{member.name!r} -> {link_target!r}"
|
||||
)
|
||||
# Hardlink names are relative to the archive root; symlink
|
||||
# targets are relative to the member's own directory.
|
||||
anchor = dest if member.islnk() else (dest / member.name).parent
|
||||
resolved_target = (anchor / link_target).resolve()
|
||||
if not resolved_target.is_relative_to(dest_resolved):
|
||||
raise ValueError(
|
||||
f"Blocked link target escaping destination: "
|
||||
f"{member.name!r} -> {link_target!r}"
|
||||
)
|
||||
tf.extractall(dest) # noqa: S202
|
||||
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def _project_script_target(script_name: str) -> str | None:
|
||||
try:
|
||||
from crewai_cli.utils import read_toml
|
||||
|
||||
pyproject = read_toml()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
target = pyproject.get("project", {}).get("scripts", {}).get(script_name)
|
||||
return target if isinstance(target, str) else None
|
||||
|
||||
|
||||
def _prepare_project_import_path() -> None:
|
||||
cwd = Path.cwd()
|
||||
for path in (cwd / "src", cwd):
|
||||
path_str = str(path)
|
||||
if path.exists() and path_str not in sys.path:
|
||||
sys.path.insert(0, path_str)
|
||||
|
||||
|
||||
def _load_conversational_flow_from_kickoff_script() -> Any | None:
|
||||
target = _project_script_target("kickoff")
|
||||
if not target or ":" not in target:
|
||||
return None
|
||||
|
||||
module_name, _callable_name = target.split(":", 1)
|
||||
_prepare_project_import_path()
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
from crewai.flow.flow import Flow
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
for value in vars(module).values():
|
||||
if (
|
||||
inspect.isclass(value)
|
||||
and value is not Flow
|
||||
and issubclass(value, Flow)
|
||||
and getattr(value, "conversational", False)
|
||||
):
|
||||
return value()
|
||||
|
||||
for value in vars(module).values():
|
||||
if (
|
||||
isinstance(value, Flow)
|
||||
and getattr(value, "conversational", False)
|
||||
and callable(getattr(value, "handle_turn", None))
|
||||
):
|
||||
return value
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _run_conversational_flow_tui(flow: Any) -> Any:
|
||||
from crewai_cli.crew_run_tui import CrewRunApp
|
||||
|
||||
app = CrewRunApp(
|
||||
crew_name=getattr(flow, "name", None) or type(flow).__name__,
|
||||
conversational=True,
|
||||
)
|
||||
app._flow = flow
|
||||
app.run()
|
||||
|
||||
if app._status == "failed":
|
||||
raise SystemExit(1)
|
||||
|
||||
return app._crew_result
|
||||
|
||||
|
||||
def kickoff_flow() -> None:
|
||||
"""
|
||||
Kickoff the flow by running a command in the UV environment.
|
||||
"""
|
||||
flow = _load_conversational_flow_from_kickoff_script()
|
||||
if flow is not None:
|
||||
_run_conversational_flow_tui(flow)
|
||||
return
|
||||
|
||||
command = ["uv", "run", "kickoff"]
|
||||
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603
|
||||
|
||||
if result.stderr:
|
||||
click.echo(result.stderr, err=True)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.echo(f"An error occurred while running the flow: {e}", err=True)
|
||||
click.echo(e.output, err=True)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||
@@ -604,16 +604,6 @@ def _run_flow_project(
|
||||
run_declarative_flow_in_project_env(definition=definition)
|
||||
return
|
||||
|
||||
from crewai_cli.kickoff_flow import (
|
||||
_load_conversational_flow_from_kickoff_script,
|
||||
_run_conversational_flow_tui,
|
||||
)
|
||||
|
||||
flow = _load_conversational_flow_from_kickoff_script()
|
||||
if flow is not None:
|
||||
_run_conversational_flow_tui(flow)
|
||||
return
|
||||
|
||||
_execute_uv_script("kickoff", entity_type="flow")
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from pydantic import ValidationError
|
||||
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials
|
||||
|
||||
|
||||
def run_declarative_flow_in_project_env(
|
||||
definition: str | Path, inputs: str | None = None
|
||||
definition: str, inputs: str | None = None
|
||||
) -> None:
|
||||
"""Run a declarative flow inside the project's Python environment."""
|
||||
if is_declarative_flow_project_env() or not _has_project_file():
|
||||
@@ -25,7 +24,7 @@ def run_declarative_flow_in_project_env(
|
||||
_execute_declarative_flow_command(["uv", "run", "crewai", "run"])
|
||||
|
||||
|
||||
def plot_declarative_flow_in_project_env(definition: str | Path) -> None:
|
||||
def plot_declarative_flow_in_project_env(definition: str) -> None:
|
||||
"""Plot a declarative flow inside the project's Python environment."""
|
||||
if is_declarative_flow_project_env() or not _has_project_file():
|
||||
plot_declarative_flow(definition=definition)
|
||||
@@ -34,7 +33,7 @@ def plot_declarative_flow_in_project_env(definition: str | Path) -> None:
|
||||
_execute_declarative_flow_command(["uv", "run", "crewai", "flow", "plot"])
|
||||
|
||||
|
||||
def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> None:
|
||||
def run_declarative_flow(definition: str, inputs: str | None = None) -> None:
|
||||
"""Run a declarative flow from a definition path."""
|
||||
parsed_inputs = _parse_inputs(inputs)
|
||||
|
||||
@@ -50,7 +49,7 @@ def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> N
|
||||
click.echo(_format_result(result))
|
||||
|
||||
|
||||
def plot_declarative_flow(definition: str | Path) -> None:
|
||||
def plot_declarative_flow(definition: str) -> None:
|
||||
"""Plot a declarative flow from a definition path."""
|
||||
try:
|
||||
flow = load_declarative_flow(definition)
|
||||
@@ -62,10 +61,11 @@ def plot_declarative_flow(definition: str | Path) -> None:
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
|
||||
def load_declarative_flow(definition: str | Path) -> Any:
|
||||
def load_declarative_flow(definition: str) -> Any:
|
||||
"""Load a declarative Flow instance from a definition path."""
|
||||
try:
|
||||
from crewai.flow.flow import Flow
|
||||
from crewai.flow.flow_definition import FlowDefinition
|
||||
except ImportError as exc:
|
||||
click.echo(
|
||||
"Running declarative flows requires the full crewai package.",
|
||||
@@ -74,36 +74,19 @@ def load_declarative_flow(definition: str | Path) -> Any:
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
definition_path = Path(definition).expanduser()
|
||||
try:
|
||||
if not definition_path.is_file():
|
||||
if definition_path.exists():
|
||||
click.echo(
|
||||
f"Invalid --definition path: {definition} is not a file.",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
click.echo(
|
||||
f"Invalid --definition path: {definition} does not exist.", err=True
|
||||
)
|
||||
raise SystemExit(1)
|
||||
except OSError as exc:
|
||||
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
|
||||
raise SystemExit(1) from exc
|
||||
definition_source = _read_declarative_flow_source(definition_path, definition)
|
||||
|
||||
try:
|
||||
return Flow.from_declaration(path=definition_path)
|
||||
except (OSError, UnicodeError, ValueError, ValidationError) as exc:
|
||||
click.echo(
|
||||
f"Unable to read --definition path {definition_path}: {exc}",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
flow_definition = _parse_declarative_flow(
|
||||
FlowDefinition,
|
||||
definition_source,
|
||||
source_path=definition_path,
|
||||
)
|
||||
return Flow.from_definition(flow_definition)
|
||||
|
||||
|
||||
def configured_project_declarative_flow(
|
||||
pyproject_data: dict[str, Any] | None = None,
|
||||
project_root: Path | None = None,
|
||||
) -> Path | None:
|
||||
) -> str | None:
|
||||
"""Return the configured declarative flow source for flow projects."""
|
||||
if pyproject_data is None:
|
||||
try:
|
||||
@@ -119,66 +102,7 @@ def configured_project_declarative_flow(
|
||||
definition = crewai_config.get("definition")
|
||||
if not isinstance(definition, str):
|
||||
return None
|
||||
definition = definition.strip()
|
||||
if not definition:
|
||||
return None
|
||||
|
||||
return _resolve_project_definition_path(
|
||||
definition=definition,
|
||||
project_root=project_root or Path.cwd(),
|
||||
)
|
||||
|
||||
|
||||
def _resolve_project_definition_path(definition: str, project_root: Path) -> Path:
|
||||
definition_path = Path(definition)
|
||||
windows_definition_path = PureWindowsPath(definition)
|
||||
|
||||
if definition.startswith("~"):
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must be a project-local path; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
if definition_path.is_absolute() or windows_definition_path.is_absolute():
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must be relative to the project root; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
try:
|
||||
root = project_root.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise click.UsageError(
|
||||
f"Invalid project root for [tool.crewai] definition: {exc}"
|
||||
) from exc
|
||||
|
||||
candidate = root / definition_path
|
||||
try:
|
||||
resolved_candidate = candidate.resolve(strict=False)
|
||||
except OSError as exc:
|
||||
raise click.UsageError(
|
||||
f"Invalid [tool.crewai] definition path {definition!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
if not resolved_candidate.is_relative_to(root):
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must resolve inside the project root; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
if not resolved_candidate.exists():
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must point to an existing file; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
if not resolved_candidate.is_file():
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must point to a regular file; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
return resolved_candidate
|
||||
return definition.strip() or None
|
||||
|
||||
|
||||
def _execute_declarative_flow_command(command: list[str]) -> None:
|
||||
@@ -230,6 +154,53 @@ def _parse_inputs(inputs: str | None) -> dict[str, Any] | None:
|
||||
return parsed
|
||||
|
||||
|
||||
def _read_declarative_flow_source(path: Path, definition: str) -> str:
|
||||
try:
|
||||
if path.is_file():
|
||||
source = _read_declarative_flow_file(path)
|
||||
elif path.exists():
|
||||
click.echo(
|
||||
f"Invalid --definition path: {definition} is not a file.", err=True
|
||||
)
|
||||
raise SystemExit(1)
|
||||
else:
|
||||
click.echo(
|
||||
f"Invalid --definition path: {definition} does not exist.", err=True
|
||||
)
|
||||
raise SystemExit(1)
|
||||
except OSError as exc:
|
||||
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
return source
|
||||
|
||||
|
||||
def _read_declarative_flow_file(path: Path) -> str:
|
||||
try:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError) as exc:
|
||||
click.echo(
|
||||
f"Unable to read --definition path {path}: {exc}",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
return source
|
||||
|
||||
|
||||
def _parse_declarative_flow(
|
||||
flow_definition_cls: type[Any], source: str, *, source_path: Path
|
||||
) -> Any:
|
||||
if _looks_like_json(source):
|
||||
return flow_definition_cls.from_json(source, source_path=source_path)
|
||||
|
||||
return flow_definition_cls.from_yaml(source, source_path=source_path)
|
||||
|
||||
|
||||
def _looks_like_json(source: str) -> bool:
|
||||
stripped = source.lstrip()
|
||||
return stripped.startswith("{")
|
||||
|
||||
|
||||
def _format_result(result: Any) -> str:
|
||||
raw_result = getattr(result, "raw", result)
|
||||
if isinstance(raw_result, str):
|
||||
|
||||
@@ -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.8a2"
|
||||
]
|
||||
|
||||
[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.8a2"
|
||||
]
|
||||
|
||||
[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.8a2"
|
||||
]
|
||||
|
||||
[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.8a2"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
"""Regression tests for path-traversal-safe archive extraction.
|
||||
|
||||
Guards against symlink/hardlink-based path traversal in the fallback used on
|
||||
Python versions without tarfile extraction filters. The filtered path relies on
|
||||
`tarfile.extractall(..., filter="data")`; the fallback must provide the same
|
||||
protection by validating link targets, not just member names.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_cli.experimental.skills.main import _safe_extractall
|
||||
|
||||
|
||||
def _tar_from_members(build) -> tarfile.TarFile:
|
||||
"""Build an in-memory tar archive via `build(tf)` and return it for reading."""
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w") as tf:
|
||||
build(tf)
|
||||
buf.seek(0)
|
||||
return tarfile.open(fileobj=buf, mode="r")
|
||||
|
||||
|
||||
def test_blocks_symlink_escaping_destination(tmp_path: Path) -> None:
|
||||
"""A symlink whose target escapes dest, plus a file written through it,
|
||||
must be rejected before anything is extracted."""
|
||||
outside = tmp_path / "outside"
|
||||
outside.mkdir()
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
link = tarfile.TarInfo("link")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = str(outside) # absolute path outside dest
|
||||
tf.addfile(link)
|
||||
payload = b"pwned"
|
||||
info = tarfile.TarInfo("link/evil.txt")
|
||||
info.size = len(payload)
|
||||
tf.addfile(info, io.BytesIO(payload))
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
with pytest.raises(ValueError, match="escaping destination"):
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
assert not (outside / "evil.txt").exists()
|
||||
|
||||
|
||||
def test_blocks_relative_symlink_escaping_destination(tmp_path: Path) -> None:
|
||||
"""A relative symlink (../..) that escapes dest is also rejected."""
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
link = tarfile.TarInfo("sub/link")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = "../../outside" # escapes dest from sub/
|
||||
tf.addfile(link)
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
with pytest.raises(ValueError, match="escaping destination"):
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
|
||||
def test_blocks_hardlink_escaping_destination(tmp_path: Path) -> None:
|
||||
"""A hardlink whose target escapes dest is rejected."""
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
link = tarfile.TarInfo("escape")
|
||||
link.type = tarfile.LNKTYPE
|
||||
link.linkname = "../outside.txt" # escapes archive root
|
||||
tf.addfile(link)
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
with pytest.raises(ValueError, match="escaping destination"):
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
|
||||
def test_blocks_special_tar_member(tmp_path: Path) -> None:
|
||||
"""Special tar members such as FIFOs are rejected."""
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
fifo = tarfile.TarInfo("pipe")
|
||||
fifo.type = tarfile.FIFOTYPE
|
||||
tf.addfile(fifo)
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
with pytest.raises(ValueError, match="unsupported tar member"):
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
|
||||
def test_allows_benign_relative_symlink(tmp_path: Path) -> None:
|
||||
"""A symlink that stays within dest is permitted."""
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
payload = b"hi"
|
||||
info = tarfile.TarInfo("real.txt")
|
||||
info.size = len(payload)
|
||||
tf.addfile(info, io.BytesIO(payload))
|
||||
link = tarfile.TarInfo("alias.txt")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = "real.txt" # stays inside dest
|
||||
tf.addfile(link)
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
assert (dest / "real.txt").read_bytes() == b"hi"
|
||||
assert (dest / "alias.txt").is_symlink()
|
||||
assert (dest / "alias.txt").readlink() == Path("real.txt")
|
||||
|
||||
|
||||
def test_allows_benign_archive(tmp_path: Path) -> None:
|
||||
"""An ordinary archive of regular files extracts correctly."""
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
for name, body in (("SKILL.md", b"# skill"), ("scripts/run.py", b"print(1)")):
|
||||
payload = body
|
||||
info = tarfile.TarInfo(name)
|
||||
info.size = len(payload)
|
||||
tf.addfile(info, io.BytesIO(payload))
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
assert (dest / "SKILL.md").read_bytes() == b"# skill"
|
||||
assert (dest / "scripts" / "run.py").read_bytes() == b"print(1)"
|
||||
@@ -126,52 +126,6 @@ def test_chain_deploy_does_not_login_for_deploy_exit(monkeypatch, capsys) -> Non
|
||||
assert "Deploy failed with exit code 42" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_conversation_turn_done_records_assistant_message() -> None:
|
||||
class RawResult:
|
||||
raw = "hello from the flow"
|
||||
|
||||
app = CrewRunApp(conversational=True)
|
||||
app._conversation_turn_in_progress = True
|
||||
app._enable_conversation_input = lambda: None # type: ignore[method-assign]
|
||||
app._tick = lambda: None # type: ignore[method-assign]
|
||||
app._scroll_to_result = lambda: None # type: ignore[method-assign]
|
||||
|
||||
app._on_conversation_turn_done(RawResult())
|
||||
|
||||
assert app._conversation_messages == [("assistant", "hello from the flow")]
|
||||
assert app._conversation_turn_in_progress is False
|
||||
assert app._status == "chatting"
|
||||
assert isinstance(app._crew_result, RawResult)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conversation_input_submits_turn() -> None:
|
||||
class FakeFlow:
|
||||
defer_trace_finalization = False
|
||||
|
||||
def handle_turn(self, message: str) -> str:
|
||||
return f"reply: {message}"
|
||||
|
||||
def finalize_session_traces(self) -> None:
|
||||
pass
|
||||
|
||||
app = CrewRunApp(crew_name="Demo", conversational=True)
|
||||
app._flow = FakeFlow()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.click("#conversation-input")
|
||||
await pilot.press("h", "i", "enter")
|
||||
for _ in range(50):
|
||||
await pilot.pause(0.05)
|
||||
if app._conversation_messages[-1:] == [("assistant", "reply: hi")]:
|
||||
break
|
||||
|
||||
assert app._conversation_messages == [
|
||||
("user", "hi"),
|
||||
("assistant", "reply: hi"),
|
||||
]
|
||||
|
||||
|
||||
def test_plan_step_status_updates_only_the_explicit_step() -> None:
|
||||
app = _app_with_plan()
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
@@ -108,8 +107,6 @@ def test_configured_project_declarative_flow(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(FLOW_YAML, encoding="utf-8")
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "flow"\ndefinition = " flow.yaml "\n',
|
||||
encoding="utf-8",
|
||||
@@ -117,132 +114,4 @@ def test_configured_project_declarative_flow(
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
assert configured_project_declarative_flow() == definition_path.resolve()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("definition", "expected_error"),
|
||||
[
|
||||
("C:/tmp/flow.yaml", "must be relative to the project root"),
|
||||
("~/flow.yaml", "must be a project-local path"),
|
||||
("../flow.yaml", "must resolve inside the project root"),
|
||||
],
|
||||
)
|
||||
def test_configured_project_declarative_flow_rejects_unsafe_paths(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
definition: str,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
f'[tool.crewai]\ntype = "flow"\ndefinition = "{definition}"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
with pytest.raises(click.UsageError) as exc_info:
|
||||
configured_project_declarative_flow()
|
||||
|
||||
assert expected_error in exc_info.value.message
|
||||
|
||||
|
||||
def test_configured_project_declarative_flow_allows_normalized_project_path(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(FLOW_YAML, encoding="utf-8")
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "flow"\ndefinition = "src/../flow.yaml"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
assert configured_project_declarative_flow() == definition_path.resolve()
|
||||
|
||||
|
||||
def test_configured_project_declarative_flow_rejects_absolute_path(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
definition = tmp_path / "flow.yaml"
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
f'[tool.crewai]\ntype = "flow"\ndefinition = "{definition.as_posix()}"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
with pytest.raises(click.UsageError) as exc_info:
|
||||
configured_project_declarative_flow()
|
||||
|
||||
assert "must be relative to the project root" in exc_info.value.message
|
||||
|
||||
|
||||
def test_configured_project_declarative_flow_rejects_symlink_escape(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
outside_definition = tmp_path.parent / "outside-flow.yaml"
|
||||
outside_definition.write_text(FLOW_YAML, encoding="utf-8")
|
||||
link = tmp_path / "flow.yaml"
|
||||
try:
|
||||
link.symlink_to(outside_definition)
|
||||
except (NotImplementedError, OSError) as exc:
|
||||
pytest.skip(f"symlinks unavailable: {exc}")
|
||||
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "flow"\ndefinition = "flow.yaml"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
with pytest.raises(click.UsageError) as exc_info:
|
||||
configured_project_declarative_flow()
|
||||
|
||||
assert "must resolve inside the project root" in exc_info.value.message
|
||||
|
||||
|
||||
def test_configured_project_declarative_flow_rejects_missing_file(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "flow"\ndefinition = "missing-flow.yaml"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
with pytest.raises(click.UsageError) as exc_info:
|
||||
configured_project_declarative_flow()
|
||||
|
||||
assert "must point to an existing file" in exc_info.value.message
|
||||
|
||||
|
||||
def test_configured_project_declarative_flow_rejects_directory(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "flow.yaml").mkdir()
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "flow"\ndefinition = "flow.yaml"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
with pytest.raises(click.UsageError) as exc_info:
|
||||
configured_project_declarative_flow()
|
||||
|
||||
assert "must point to a regular file" in exc_info.value.message
|
||||
assert configured_project_declarative_flow() == "flow.yaml"
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from crewai_cli import kickoff_flow
|
||||
|
||||
|
||||
def test_loads_conversational_flow_from_kickoff_script(tmp_path, monkeypatch) -> None:
|
||||
package_dir = tmp_path / "src" / "demo_chat"
|
||||
package_dir.mkdir(parents=True)
|
||||
(package_dir / "__init__.py").write_text("")
|
||||
(package_dir / "main.py").write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"from crewai.flow import Flow",
|
||||
"",
|
||||
"class DemoChatFlow(Flow):",
|
||||
" conversational = True",
|
||||
]
|
||||
)
|
||||
)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"[project]",
|
||||
'name = "demo-chat"',
|
||||
"[project.scripts]",
|
||||
'kickoff = "demo_chat.main:kickoff"',
|
||||
]
|
||||
)
|
||||
)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
sys.modules.pop("demo_chat.main", None)
|
||||
sys.modules.pop("demo_chat", None)
|
||||
|
||||
flow = kickoff_flow._load_conversational_flow_from_kickoff_script()
|
||||
|
||||
assert flow is not None
|
||||
assert type(flow).__name__ == "DemoChatFlow"
|
||||
assert flow.conversational is True
|
||||
|
||||
|
||||
def test_kickoff_flow_falls_back_to_uv_when_no_conversational_flow(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
calls: list[list[str]] = []
|
||||
|
||||
def fake_run(command, capture_output, text, check):
|
||||
calls.append(command)
|
||||
|
||||
class Result:
|
||||
stderr = ""
|
||||
|
||||
return Result()
|
||||
|
||||
monkeypatch.setattr(
|
||||
kickoff_flow, "_load_conversational_flow_from_kickoff_script", lambda: None
|
||||
)
|
||||
monkeypatch.setattr(kickoff_flow.subprocess, "run", fake_run)
|
||||
|
||||
kickoff_flow.kickoff_flow()
|
||||
|
||||
assert calls == [["uv", "run", "kickoff"]]
|
||||
@@ -645,10 +645,6 @@ def test_run_crew_runs_python_flow_project(monkeypatch, capsys):
|
||||
"_execute_uv_script",
|
||||
lambda script_name, **kwargs: calls.append((script_name, kwargs)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._load_conversational_flow_from_kickoff_script",
|
||||
lambda: None,
|
||||
)
|
||||
|
||||
run_crew_module.run_crew()
|
||||
|
||||
@@ -656,41 +652,6 @@ def test_run_crew_runs_python_flow_project(monkeypatch, capsys):
|
||||
assert calls == [("kickoff", {"entity_type": "flow"})]
|
||||
|
||||
|
||||
def test_run_crew_runs_conversational_flow_tui(monkeypatch, capsys):
|
||||
class Flow:
|
||||
pass
|
||||
|
||||
flow = Flow()
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
lambda: {"tool": {"crewai": {"type": "flow"}}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._load_conversational_flow_from_kickoff_script",
|
||||
lambda: flow,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._run_conversational_flow_tui",
|
||||
lambda loaded_flow: calls.append(loaded_flow),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_execute_uv_script",
|
||||
lambda *_args, **_kwargs: pytest.fail(
|
||||
"conversational flows must use the TUI"
|
||||
),
|
||||
)
|
||||
|
||||
run_crew_module.run_crew()
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
assert calls == [flow]
|
||||
|
||||
|
||||
def test_run_crew_rejects_filename_for_flow_project(monkeypatch):
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
@@ -705,14 +666,9 @@ def test_run_crew_rejects_filename_for_flow_project(monkeypatch):
|
||||
assert "--filename can only be used when running crews" in exc_info.value.message
|
||||
|
||||
|
||||
def test_run_crew_runs_configured_declarative_flow_project(
|
||||
monkeypatch, tmp_path: Path, capsys
|
||||
):
|
||||
def test_run_crew_runs_configured_declarative_flow_project(monkeypatch, capsys):
|
||||
calls = []
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\n", encoding="utf-8")
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
@@ -739,4 +695,4 @@ def test_run_crew_runs_configured_declarative_flow_project(
|
||||
run_crew_module.run_crew()
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
assert calls == [(definition_path.resolve(), None)]
|
||||
assert calls == [("flow.yaml", None)]
|
||||
|
||||
@@ -60,43 +60,6 @@ def test_run_declarative_flow_reports_missing_file(
|
||||
)
|
||||
|
||||
|
||||
def test_run_declarative_flow_reports_empty_file(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(" \n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow_module.run_declarative_flow(str(definition_path))
|
||||
|
||||
assert "Flow declaration file is empty" in capsys.readouterr().err
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"contents, expected_error",
|
||||
[
|
||||
("[]\n", "Flow declaration must contain a mapping"),
|
||||
("schema: crewai.flow/v1\nmethods: {}\n", "Field required"),
|
||||
],
|
||||
)
|
||||
def test_load_declarative_flow_reports_invalid_declarations(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
contents: str,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(contents, encoding="utf-8")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_declarative_flow_module.load_declarative_flow(str(definition_path))
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
stderr = capsys.readouterr().err
|
||||
assert f"Unable to read --definition path {definition_path}:" in stderr
|
||||
assert expected_error in stderr
|
||||
|
||||
|
||||
def test_run_declarative_flow_in_project_env_uses_uv(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
||||
@@ -16,9 +16,9 @@ dependencies = [
|
||||
"pyjwt>=2.13.0,<3",
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"rich>=13.7.1",
|
||||
"opentelemetry-api~=1.42.0",
|
||||
"opentelemetry-sdk~=1.42.0",
|
||||
"opentelemetry-exporter-otlp-proto-http~=1.42.0",
|
||||
"opentelemetry-api~=1.34.0",
|
||||
"opentelemetry-sdk~=1.34.0",
|
||||
"opentelemetry-exporter-otlp-proto-http~=1.34.0",
|
||||
"tomli~=2.0.2",
|
||||
]
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a2"
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a2"
|
||||
|
||||
@@ -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.8a2",
|
||||
"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.8a2"
|
||||
|
||||
@@ -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.8a2",
|
||||
"crewai-cli==1.14.8a2",
|
||||
# Core Dependencies
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"openai>=2.30.0,<3",
|
||||
@@ -18,9 +18,9 @@ dependencies = [
|
||||
"pdfplumber~=0.11.4",
|
||||
"regex~=2026.1.15",
|
||||
# Telemetry and Monitoring
|
||||
"opentelemetry-api~=1.42.0",
|
||||
"opentelemetry-sdk~=1.42.0",
|
||||
"opentelemetry-exporter-otlp-proto-http~=1.42.0",
|
||||
"opentelemetry-api~=1.34.0",
|
||||
"opentelemetry-sdk~=1.34.0",
|
||||
"opentelemetry-exporter-otlp-proto-http~=1.34.0",
|
||||
# Data Handling
|
||||
"chromadb~=1.1.0",
|
||||
"tokenizers>=0.21,<1",
|
||||
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.14.8a4",
|
||||
"crewai-tools==1.14.8a2",
|
||||
]
|
||||
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.8a2"
|
||||
|
||||
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"Memory": ("crewai.memory.unified_memory", "Memory"),
|
||||
|
||||
@@ -9,7 +9,6 @@ from __future__ import annotations
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
from typing import TypedDict
|
||||
@@ -128,36 +127,12 @@ class SkillCacheManager:
|
||||
|
||||
|
||||
def _safe_extractall(tf: tarfile.TarFile, dest: Path) -> None:
|
||||
"""Path-traversal-safe extraction for Python versions without tar filters.
|
||||
|
||||
Validates both the member's own path and, for symlink/hardlink members,
|
||||
the link target. Without the link-target check a malicious archive can
|
||||
plant a symlink that escapes ``dest`` followed by a regular member written
|
||||
through that link, escaping ``dest`` even though every member name resolves
|
||||
inside it. This mirrors the protection that
|
||||
``tarfile.extractall(..., filter="data")`` provides when available.
|
||||
"""
|
||||
"""Path-traversal-safe extraction for Python < 3.12."""
|
||||
dest_resolved = dest.resolve()
|
||||
for member in tf.getmembers():
|
||||
member_path = (dest / member.name).resolve()
|
||||
if not member_path.is_relative_to(dest_resolved):
|
||||
raise ValueError(f"Blocked path traversal attempt: {member.name!r}")
|
||||
if not (member.isfile() or member.isdir() or member.issym() or member.islnk()):
|
||||
raise ValueError(f"Blocked unsupported tar member: {member.name!r}")
|
||||
if member.issym() or member.islnk():
|
||||
link_target = member.linkname
|
||||
if os.path.isabs(link_target):
|
||||
raise ValueError(
|
||||
f"Blocked link target escaping destination: "
|
||||
f"{member.name!r} -> {link_target!r}"
|
||||
)
|
||||
anchor = dest if member.islnk() else (dest / member.name).parent
|
||||
resolved_target = (anchor / link_target).resolve()
|
||||
if not resolved_target.is_relative_to(dest_resolved):
|
||||
raise ValueError(
|
||||
f"Blocked link target escaping destination: "
|
||||
f"{member.name!r} -> {link_target!r}"
|
||||
)
|
||||
tf.extractall(dest) # noqa: S202
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Flow Definition: the serializable, declarative Flow contract.
|
||||
|
||||
Defines :class:`FlowDefinition` and its sub-models — a static declarative
|
||||
representation of a Flow: its methods, trigger conditions,
|
||||
Defines :class:`FlowDefinition` and its sub-models — a static, declarative
|
||||
(JSON/YAML) representation of a Flow: its methods, trigger conditions,
|
||||
state, and configuration. It is independent of the Python authoring
|
||||
layer that may have produced it and of the engine that runs it (see
|
||||
``runtime``).
|
||||
@@ -235,7 +235,7 @@ class FlowPersistenceDefinition(BaseModel):
|
||||
|
||||
``persistence`` may hold a live backend when the definition is built from
|
||||
a decorated class — the engine then persists through the exact instance
|
||||
the user configured; the declarative projection degrades it to its
|
||||
the user configured; the JSON/YAML projection degrades it to its
|
||||
serialized config.
|
||||
"""
|
||||
|
||||
@@ -275,7 +275,7 @@ class FlowHumanFeedbackDefinition(BaseModel):
|
||||
"""Static human feedback configuration.
|
||||
|
||||
``llm`` and ``provider`` may hold live Python objects when the definition
|
||||
is built from a decorated class; the declarative projection degrades them to
|
||||
is built from a decorated class; the JSON/YAML projection degrades them to
|
||||
a serialized config (``llm``) or a ``module:qualname`` ref (``provider``).
|
||||
"""
|
||||
|
||||
@@ -777,7 +777,7 @@ class FlowDefinition(BaseModel):
|
||||
return self
|
||||
|
||||
def to_dict(self, *, exclude_none: bool = True) -> dict[str, Any]:
|
||||
"""Serialize the definition to a declaration-ready dictionary."""
|
||||
"""Serialize the definition to a JSON/YAML-ready dictionary."""
|
||||
return self.model_dump(by_alias=True, exclude_none=exclude_none, mode="json")
|
||||
|
||||
def to_json(self, *, indent: int | None = 2, exclude_none: bool = True) -> str:
|
||||
@@ -817,37 +817,16 @@ class FlowDefinition(BaseModel):
|
||||
return definition
|
||||
|
||||
@classmethod
|
||||
def from_declaration(
|
||||
cls,
|
||||
*,
|
||||
contents: FlowDefinition | str | dict[str, Any] | None = None,
|
||||
path: Path | str | None = None,
|
||||
) -> FlowDefinition:
|
||||
"""Load a declarative flow from contents or a file path."""
|
||||
if isinstance(contents, cls):
|
||||
return contents
|
||||
def from_json(cls, data: str, *, source_path: Path | None = None) -> FlowDefinition:
|
||||
"""Load a definition from JSON."""
|
||||
return cls.from_dict(json.loads(data), source_path=source_path)
|
||||
|
||||
source_path: Path | None = None
|
||||
if contents is None:
|
||||
if path is None:
|
||||
raise ValueError("Provide contents or path")
|
||||
source_path = Path(path)
|
||||
contents = source_path.expanduser().read_text(encoding="utf-8")
|
||||
|
||||
if isinstance(contents, dict):
|
||||
return cls.from_dict(contents)
|
||||
|
||||
if not isinstance(contents, str):
|
||||
raise TypeError("Flow declaration contents must be a string or dictionary")
|
||||
|
||||
if not contents.strip():
|
||||
if source_path is not None:
|
||||
raise ValueError(f"Flow declaration file is empty: {source_path}")
|
||||
raise ValueError("Flow declaration contents are empty")
|
||||
|
||||
loaded = yaml.safe_load(contents)
|
||||
@classmethod
|
||||
def from_yaml(cls, data: str, *, source_path: Path | None = None) -> FlowDefinition:
|
||||
"""Load a definition from YAML."""
|
||||
loaded = yaml.safe_load(data) or {}
|
||||
if not isinstance(loaded, dict):
|
||||
raise ValueError("Flow declaration must contain a mapping")
|
||||
raise ValueError("Flow definition YAML must contain a mapping")
|
||||
return cls.from_dict(loaded, source_path=source_path)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -25,7 +25,6 @@ from datetime import datetime
|
||||
import enum
|
||||
import inspect
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import threading
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -770,21 +769,6 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
@classmethod
|
||||
def from_definition(cls, definition: FlowDefinition, **kwargs: Any) -> Flow[Any]:
|
||||
"""Build a runnable Flow directly from a definition; no subclass required."""
|
||||
return cls.from_declaration(contents=definition, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_declaration(
|
||||
cls,
|
||||
*,
|
||||
contents: FlowDefinition | str | dict[str, Any] | None = None,
|
||||
path: Path | str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Flow[Any]:
|
||||
"""Build a runnable declarative flow from contents or a file path."""
|
||||
definition = FlowDefinition.from_declaration(
|
||||
contents=contents,
|
||||
path=path,
|
||||
)
|
||||
return cls.model_validate(
|
||||
{**definition.config.model_dump(), **kwargs},
|
||||
context={"flow_definition": definition},
|
||||
|
||||
@@ -8,9 +8,7 @@ import json
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.experimental.skills.cache import SkillCacheManager, _safe_extractall
|
||||
from crewai.experimental.skills.cache import SkillCacheManager
|
||||
|
||||
|
||||
def _make_tar_gz(files: dict[str, str]) -> bytes:
|
||||
@@ -37,15 +35,6 @@ def _make_tar_gz(files: dict[str, str]) -> bytes:
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def _tar_from_members(build) -> tarfile.TarFile:
|
||||
"""Build an in-memory tar archive via `build(tf)` and return it for reading."""
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w") as tf:
|
||||
build(tf)
|
||||
buf.seek(0)
|
||||
return tarfile.open(fileobj=buf, mode="r")
|
||||
|
||||
|
||||
class TestSkillCacheManager:
|
||||
def test_get_cached_path_missing(self, tmp_path: Path) -> None:
|
||||
cache = SkillCacheManager(cache_root=tmp_path)
|
||||
@@ -124,85 +113,3 @@ class TestSkillCacheManager:
|
||||
dest = cache.store("acme", "my-skill", None, archive)
|
||||
meta = json.loads((dest / ".crewai_meta.json").read_text())
|
||||
assert meta["version"] is None
|
||||
|
||||
|
||||
def test_safe_extractall_blocks_symlink_escaping_cache_destination(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A symlink whose target escapes dest is rejected before extraction."""
|
||||
outside = tmp_path / "outside"
|
||||
outside.mkdir()
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
link = tarfile.TarInfo("link")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = str(outside)
|
||||
tf.addfile(link)
|
||||
payload = b"pwned"
|
||||
info = tarfile.TarInfo("link/evil.txt")
|
||||
info.size = len(payload)
|
||||
tf.addfile(info, io.BytesIO(payload))
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
with pytest.raises(ValueError, match="escaping destination"):
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
assert not (outside / "evil.txt").exists()
|
||||
|
||||
|
||||
def test_safe_extractall_blocks_hardlink_escaping_cache_destination(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A hardlink whose target escapes dest is rejected."""
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
link = tarfile.TarInfo("escape")
|
||||
link.type = tarfile.LNKTYPE
|
||||
link.linkname = "../outside.txt"
|
||||
tf.addfile(link)
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
with pytest.raises(ValueError, match="escaping destination"):
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
|
||||
def test_safe_extractall_blocks_special_cache_tar_member(tmp_path: Path) -> None:
|
||||
"""Special tar members such as FIFOs are rejected."""
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
fifo = tarfile.TarInfo("pipe")
|
||||
fifo.type = tarfile.FIFOTYPE
|
||||
tf.addfile(fifo)
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
with pytest.raises(ValueError, match="unsupported tar member"):
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
|
||||
def test_safe_extractall_allows_benign_cache_symlink(tmp_path: Path) -> None:
|
||||
"""A symlink that stays within dest is permitted."""
|
||||
dest = tmp_path / "dest"
|
||||
dest.mkdir()
|
||||
|
||||
def build(tf: tarfile.TarFile) -> None:
|
||||
payload = b"hi"
|
||||
info = tarfile.TarInfo("real.txt")
|
||||
info.size = len(payload)
|
||||
tf.addfile(info, io.BytesIO(payload))
|
||||
link = tarfile.TarInfo("alias.txt")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = "real.txt"
|
||||
tf.addfile(link)
|
||||
|
||||
with _tar_from_members(build) as tf:
|
||||
_safe_extractall(tf, dest)
|
||||
|
||||
assert (dest / "real.txt").read_bytes() == b"hi"
|
||||
assert (dest / "alias.txt").is_symlink()
|
||||
assert (dest / "alias.txt").readlink() == Path("real.txt")
|
||||
|
||||
@@ -613,7 +613,7 @@ def test_flow_definition_merges_stacked_listen_router():
|
||||
assert methods["second_router"].emit == ["second_approval", "not_approved"]
|
||||
|
||||
|
||||
def test_flow_definition_round_trips_declaration_serialization():
|
||||
def test_flow_definition_round_trips_json_and_yaml():
|
||||
class RoundTripFlow(Flow):
|
||||
@start()
|
||||
def begin(self):
|
||||
@@ -629,122 +629,16 @@ def test_flow_definition_round_trips_declaration_serialization():
|
||||
|
||||
definition = RoundTripFlow.flow_definition()
|
||||
|
||||
round_trips = [
|
||||
flow_definition.FlowDefinition.from_declaration(contents=definition.to_json()),
|
||||
flow_definition.FlowDefinition.from_declaration(contents=definition.to_yaml()),
|
||||
]
|
||||
json_round_trip = flow_definition.FlowDefinition.from_json(definition.to_json())
|
||||
yaml_round_trip = flow_definition.FlowDefinition.from_yaml(definition.to_yaml())
|
||||
|
||||
for round_trip in round_trips:
|
||||
assert round_trip.to_dict() == definition.to_dict()
|
||||
assert round_trip.methods["decide"].router is True
|
||||
assert round_trip.methods["decide"].listen == "begin"
|
||||
assert json_round_trip.to_dict() == definition.to_dict()
|
||||
assert yaml_round_trip.to_dict() == definition.to_dict()
|
||||
assert yaml_round_trip.methods["decide"].router is True
|
||||
assert yaml_round_trip.methods["decide"].listen == "begin"
|
||||
|
||||
|
||||
def test_flow_definition_from_declaration_accepts_contents():
|
||||
data = {
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "DeclarationFlow",
|
||||
"methods": {
|
||||
"begin": {
|
||||
"start": True,
|
||||
"do": {
|
||||
"call": "expression",
|
||||
"expr": "'started'",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
definition = flow_definition.FlowDefinition.from_dict(data)
|
||||
contents = [
|
||||
definition,
|
||||
data,
|
||||
definition.to_json(),
|
||||
definition.to_yaml(),
|
||||
]
|
||||
expected = definition.to_dict()
|
||||
|
||||
for content in contents:
|
||||
loaded = flow_definition.FlowDefinition.from_declaration(contents=content)
|
||||
|
||||
assert loaded.to_dict() == expected
|
||||
|
||||
|
||||
def test_flow_definition_from_declaration_rejects_empty_file(tmp_path: Path):
|
||||
declaration_path = tmp_path / "flow.crewai"
|
||||
declaration_path.write_text(" \n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="Flow declaration file is empty"):
|
||||
flow_definition.FlowDefinition.from_declaration(path=declaration_path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("contents", ["[]", "false", "0", "null", "~"])
|
||||
def test_flow_definition_from_declaration_rejects_falsey_non_mapping_contents(
|
||||
contents: str,
|
||||
):
|
||||
with pytest.raises(ValueError, match="Flow declaration must contain a mapping"):
|
||||
flow_definition.FlowDefinition.from_declaration(contents=contents)
|
||||
|
||||
|
||||
def test_flow_definition_from_declaration_accepts_paths(tmp_path: Path):
|
||||
definition = flow_definition.FlowDefinition.from_dict(
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "DeclarationFlow",
|
||||
"methods": {
|
||||
"begin": {
|
||||
"start": True,
|
||||
"do": {
|
||||
"call": "expression",
|
||||
"expr": "'started'",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
declaration_path = tmp_path / "flow.crewai"
|
||||
declaration_path.write_text(definition.to_yaml(), encoding="utf-8")
|
||||
path_inputs = [
|
||||
declaration_path,
|
||||
str(declaration_path),
|
||||
]
|
||||
|
||||
for path_input in path_inputs:
|
||||
loaded = flow_definition.FlowDefinition.from_declaration(path=path_input)
|
||||
|
||||
assert loaded.to_dict() == definition.to_dict()
|
||||
assert loaded.source_path == declaration_path.resolve()
|
||||
|
||||
|
||||
def test_flow_definition_from_declaration_requires_input():
|
||||
with pytest.raises(ValueError, match="Provide contents or path"):
|
||||
flow_definition.FlowDefinition.from_declaration()
|
||||
|
||||
|
||||
def test_flow_definition_from_declaration_prefers_contents_over_path(
|
||||
tmp_path: Path,
|
||||
):
|
||||
data = {
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ContentsFlow",
|
||||
"methods": {
|
||||
"begin": {
|
||||
"start": True,
|
||||
"do": {"call": "expression", "expr": "'started'"},
|
||||
},
|
||||
},
|
||||
}
|
||||
declaration_path = tmp_path / "missing.crewai"
|
||||
|
||||
loaded = flow_definition.FlowDefinition.from_declaration(
|
||||
contents=data,
|
||||
path=declaration_path,
|
||||
)
|
||||
|
||||
assert loaded.name == "ContentsFlow"
|
||||
assert loaded.source_path is None
|
||||
|
||||
|
||||
def test_each_action_round_trips_declaration_serialization():
|
||||
def test_each_action_round_trips_json_and_yaml():
|
||||
definition = flow_definition.FlowDefinition.from_dict(
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
@@ -783,17 +677,15 @@ def test_each_action_round_trips_declaration_serialization():
|
||||
}
|
||||
)
|
||||
|
||||
round_trips = [
|
||||
flow_definition.FlowDefinition.from_declaration(contents=definition.to_json()),
|
||||
flow_definition.FlowDefinition.from_declaration(contents=definition.to_yaml()),
|
||||
]
|
||||
json_round_trip = flow_definition.FlowDefinition.from_json(definition.to_json())
|
||||
yaml_round_trip = flow_definition.FlowDefinition.from_yaml(definition.to_yaml())
|
||||
|
||||
for round_trip in round_trips:
|
||||
assert round_trip.to_dict() == definition.to_dict()
|
||||
assert round_trip.methods["process_rows"].description == (
|
||||
"Process every loaded row."
|
||||
)
|
||||
assert round_trip.methods["process_rows"].do.call == "each"
|
||||
assert json_round_trip.to_dict() == definition.to_dict()
|
||||
assert yaml_round_trip.to_dict() == definition.to_dict()
|
||||
assert yaml_round_trip.methods["process_rows"].description == (
|
||||
"Process every loaded row."
|
||||
)
|
||||
assert yaml_round_trip.methods["process_rows"].do.call == "each"
|
||||
|
||||
|
||||
def test_flow_definition_rejects_invalid_method_names():
|
||||
|
||||
@@ -454,7 +454,7 @@ def assert_parity(flow_cls, yaml_str, inputs=None, ordered=True):
|
||||
class_flow = flow_cls()
|
||||
class_result, class_events = _run_with_events(class_flow, inputs)
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=yaml_str)
|
||||
definition = FlowDefinition.from_yaml(yaml_str)
|
||||
definition_flow = Flow.from_definition(definition)
|
||||
definition_result, definition_events = _run_with_events(definition_flow, inputs)
|
||||
|
||||
@@ -477,21 +477,6 @@ def test_simple_chain_parity():
|
||||
assert flow.method_outputs == ["hello", "HELLO", "confirmed:True"]
|
||||
|
||||
|
||||
def test_flow_from_declaration_builds_runnable_flow():
|
||||
flow = Flow.from_declaration(contents=CHAIN_YAML)
|
||||
|
||||
assert flow.kickoff() == "confirmed:True"
|
||||
assert flow.method_outputs == ["hello", "HELLO", "confirmed:True"]
|
||||
|
||||
|
||||
def test_flow_from_declaration_accepts_flow_definition():
|
||||
definition = FlowDefinition.from_declaration(contents=CHAIN_YAML)
|
||||
flow = Flow.from_declaration(contents=definition)
|
||||
|
||||
assert flow.kickoff() == "confirmed:True"
|
||||
assert flow.method_outputs == ["hello", "HELLO", "confirmed:True"]
|
||||
|
||||
|
||||
def test_and_or_merge_parity():
|
||||
flow, _ = assert_parity(MergeFlow, MERGE_YAML, ordered=False)
|
||||
assert flow.state["joined"] is True
|
||||
@@ -514,7 +499,7 @@ def test_cyclic_flow_parity():
|
||||
|
||||
|
||||
def test_definition_flow_events_use_definition_name():
|
||||
definition = FlowDefinition.from_declaration(contents=CHAIN_YAML)
|
||||
definition = FlowDefinition.from_yaml(CHAIN_YAML)
|
||||
flow = Flow.from_definition(definition)
|
||||
_, events = _run_with_events(flow)
|
||||
assert events
|
||||
@@ -603,7 +588,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff() == "found:ai agents"
|
||||
|
||||
@@ -654,7 +639,7 @@ methods:
|
||||
listen: begin
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "ai"}) == "found:ai agents"
|
||||
|
||||
@@ -773,7 +758,7 @@ methods:
|
||||
listen: begin
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff() == "search:hello agents"
|
||||
|
||||
@@ -798,7 +783,7 @@ methods:
|
||||
listen: build_query
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff() == "found:ai agents news"
|
||||
|
||||
@@ -818,7 +803,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert (
|
||||
flow.kickoff(inputs={"limit": 2, "domains": ["crewai.com", "example.com"]})
|
||||
@@ -851,7 +836,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"question": "What is CrewAI?"}) == {
|
||||
"agent": "Analyst",
|
||||
@@ -889,7 +874,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"questions": ["one", "two"]}) == [
|
||||
"Analyst:one",
|
||||
@@ -920,7 +905,7 @@ def test_agent_action_round_trips_with_inline_definition():
|
||||
}
|
||||
)
|
||||
|
||||
round_trip = FlowDefinition.from_declaration(contents=definition.to_yaml())
|
||||
round_trip = FlowDefinition.from_yaml(definition.to_yaml())
|
||||
action = round_trip.to_dict()["methods"]["answer"]["do"]
|
||||
|
||||
assert action["call"] == "agent"
|
||||
@@ -983,7 +968,7 @@ methods:
|
||||
"""
|
||||
|
||||
with pytest.raises(ValidationError, match="invalid CEL expression"):
|
||||
FlowDefinition.from_declaration(contents=yaml_str)
|
||||
FlowDefinition.from_yaml(yaml_str)
|
||||
|
||||
|
||||
def test_crew_action_runs_inline_yaml_definition(monkeypatch: pytest.MonkeyPatch):
|
||||
@@ -1025,7 +1010,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "inline_research",
|
||||
@@ -1101,7 +1086,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "referenced_research",
|
||||
@@ -1175,7 +1160,9 @@ methods:
|
||||
other_cwd.mkdir()
|
||||
monkeypatch.chdir(other_cwd)
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(path=flow_path))
|
||||
flow = Flow.from_definition(
|
||||
FlowDefinition.from_yaml(yaml_str, source_path=flow_path)
|
||||
)
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "relative_research",
|
||||
@@ -1198,9 +1185,10 @@ methods:
|
||||
from_declaration: ../outside/crew.jsonc
|
||||
start: true
|
||||
"""
|
||||
flow_path.write_text(yaml_str, encoding="utf-8")
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(path=flow_path))
|
||||
flow = Flow.from_definition(
|
||||
FlowDefinition.from_yaml(yaml_str, source_path=flow_path)
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
@@ -1423,7 +1411,7 @@ methods:
|
||||
"""
|
||||
|
||||
with pytest.raises(ValidationError, match="invalid CEL expression"):
|
||||
FlowDefinition.from_declaration(contents=yaml_str)
|
||||
FlowDefinition.from_yaml(yaml_str)
|
||||
|
||||
|
||||
def test_code_action_renders_keyword_inputs():
|
||||
@@ -1441,7 +1429,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"name": "hello"}) == "hello!"
|
||||
|
||||
@@ -1460,7 +1448,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"value": "ok"}) == "callable:ok"
|
||||
|
||||
@@ -1484,7 +1472,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
|
||||
"normalized:a",
|
||||
@@ -1511,7 +1499,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
caller_thread_id = threading.get_ident()
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a"]}) == ["process_rows:a"]
|
||||
@@ -1538,7 +1526,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == ["async:a", "async:b"]
|
||||
|
||||
@@ -1560,7 +1548,7 @@ methods:
|
||||
FlowScriptExecutionDisabledError,
|
||||
match="CREWAI_ALLOW_FLOW_SCRIPT_EXECUTION=1",
|
||||
) as exc_info:
|
||||
Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
assert "methods with unresolvable actions" not in str(exc_info.value)
|
||||
|
||||
|
||||
@@ -1584,7 +1572,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"raw_score": 3.2}) == "rounded:4"
|
||||
assert flow.state["rounded"] == 4
|
||||
@@ -1613,7 +1601,7 @@ methods:
|
||||
listen: seed
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff() == "alpha:alpha"
|
||||
assert flow.state["input_matches_output"] is True
|
||||
@@ -1651,7 +1639,7 @@ methods:
|
||||
listen: seed
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"rows": [" a ", " b "]}) == ["global:a", "global:b"]
|
||||
|
||||
@@ -1683,7 +1671,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
|
||||
{"row": "a", "normalized": "saved:a"},
|
||||
@@ -1712,7 +1700,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == ["a", "b"]
|
||||
assert flow._method_outputs == [
|
||||
@@ -1750,7 +1738,7 @@ methods:
|
||||
listen: seed
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
|
||||
"local:a",
|
||||
@@ -1789,7 +1777,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(
|
||||
inputs={
|
||||
@@ -1823,7 +1811,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(inputs={"rows": [{"kind": "keep", "value": "a"}]}) == ["a"]
|
||||
|
||||
@@ -1850,7 +1838,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
assert flow.kickoff(
|
||||
inputs={
|
||||
@@ -1880,7 +1868,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
with pytest.raises(ValueError, match="if expression must evaluate to a boolean"):
|
||||
flow.kickoff(inputs={"rows": [{"value": "truthy"}]})
|
||||
@@ -1910,7 +1898,7 @@ methods:
|
||||
listen: process_rows
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
events = []
|
||||
with crewai_event_bus.scoped_handlers():
|
||||
|
||||
@@ -2081,7 +2069,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
with pytest.raises(RuntimeError, match="bad row"):
|
||||
flow.kickoff(inputs={"rows": ["ok", "bad"]})
|
||||
@@ -2202,7 +2190,7 @@ methods:
|
||||
listen: right
|
||||
"""
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=yaml_str)
|
||||
definition = FlowDefinition.from_yaml(yaml_str)
|
||||
|
||||
assert Flow.from_definition(definition).kickoff(
|
||||
inputs={"direction": "left"}
|
||||
@@ -2225,7 +2213,7 @@ methods:
|
||||
"""
|
||||
|
||||
with pytest.raises(ValidationError, match="invalid CEL expression"):
|
||||
FlowDefinition.from_declaration(contents=yaml_str)
|
||||
FlowDefinition.from_yaml(yaml_str)
|
||||
|
||||
|
||||
def test_expression_action_rejects_unknown_cel_root():
|
||||
@@ -2241,7 +2229,7 @@ methods:
|
||||
"""
|
||||
|
||||
with pytest.raises(ValidationError, match="unknown CEL root"):
|
||||
FlowDefinition.from_declaration(contents=yaml_str)
|
||||
FlowDefinition.from_yaml(yaml_str)
|
||||
|
||||
|
||||
def test_tool_action_requires_module_qualname_ref():
|
||||
@@ -2275,16 +2263,14 @@ def test_pydantic_state_from_ref_parity():
|
||||
|
||||
|
||||
def test_pydantic_state_default_overlay():
|
||||
flow = Flow.from_definition(
|
||||
FlowDefinition.from_declaration(contents=PYDANTIC_STATE_OVERLAY_YAML)
|
||||
)
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(PYDANTIC_STATE_OVERLAY_YAML))
|
||||
result = flow.kickoff()
|
||||
assert result == "count=6"
|
||||
assert flow.state.count == 6
|
||||
|
||||
|
||||
def test_json_schema_state():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=JSON_SCHEMA_STATE_YAML))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(JSON_SCHEMA_STATE_YAML))
|
||||
result = flow.kickoff()
|
||||
assert result == "count=1"
|
||||
assert flow.state.count == 1
|
||||
@@ -2293,14 +2279,14 @@ def test_json_schema_state():
|
||||
|
||||
|
||||
def test_json_schema_state_validates_inputs():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=JSON_SCHEMA_STATE_YAML))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(JSON_SCHEMA_STATE_YAML))
|
||||
with pytest.raises(ValueError, match="Invalid inputs"):
|
||||
flow.kickoff(inputs={"count": "not-a-number"})
|
||||
|
||||
|
||||
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)
|
||||
FlowDefinition.from_yaml(PYDANTIC_REF_WITH_SCHEMA_FALLBACK_YAML)
|
||||
)
|
||||
result = flow.kickoff()
|
||||
assert result == "count=1"
|
||||
@@ -2309,9 +2295,7 @@ def test_pydantic_state_falls_back_to_json_schema_when_ref_unimportable():
|
||||
|
||||
def test_pydantic_state_without_ref_or_schema_falls_back_to_dict(caplog):
|
||||
with caplog.at_level("ERROR"):
|
||||
flow = Flow.from_definition(
|
||||
FlowDefinition.from_declaration(contents=UNRESOLVABLE_STATE_YAML)
|
||||
)
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(UNRESOLVABLE_STATE_YAML))
|
||||
assert "falling back to dict state" in caplog.text
|
||||
|
||||
result = flow.kickoff()
|
||||
@@ -2321,7 +2305,7 @@ def test_pydantic_state_without_ref_or_schema_falls_back_to_dict(caplog):
|
||||
|
||||
|
||||
def test_dict_state_is_a_copy_of_default_plus_id():
|
||||
definition = FlowDefinition.from_declaration(contents=DICT_STATE_YAML)
|
||||
definition = FlowDefinition.from_yaml(DICT_STATE_YAML)
|
||||
|
||||
flow = Flow.from_definition(definition)
|
||||
assert flow.state["count"] == 5
|
||||
@@ -2338,7 +2322,7 @@ def test_dict_state_is_a_copy_of_default_plus_id():
|
||||
|
||||
def test_unknown_state_type_falls_back_to_dict(caplog):
|
||||
with caplog.at_level("WARNING"):
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=UNKNOWN_STATE_YAML))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(UNKNOWN_STATE_YAML))
|
||||
assert "falling back to dict state" in caplog.text
|
||||
|
||||
result = flow.kickoff()
|
||||
@@ -2411,7 +2395,7 @@ def _run_capturing_flow_lifecycle(yaml_str, event_types):
|
||||
def capture(source, event):
|
||||
events.append(event)
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
result = flow.kickoff()
|
||||
return flow, result, events
|
||||
|
||||
@@ -2425,7 +2409,7 @@ _LIFECYCLE_EVENTS = [
|
||||
]
|
||||
|
||||
|
||||
def test_config_suppress_flow_events_from_declaration():
|
||||
def test_config_suppress_flow_events_from_yaml():
|
||||
twin_events = []
|
||||
with crewai_event_bus.scoped_handlers():
|
||||
for event_type in _LIFECYCLE_EVENTS:
|
||||
@@ -2448,14 +2432,14 @@ def test_config_suppress_flow_events_from_declaration():
|
||||
)
|
||||
|
||||
|
||||
def test_config_max_method_calls_from_declaration():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=CAPPED_LOOP_YAML))
|
||||
def test_config_max_method_calls_from_yaml():
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(CAPPED_LOOP_YAML))
|
||||
with pytest.raises(RecursionError, match="has been called 2 times"):
|
||||
flow.kickoff()
|
||||
|
||||
|
||||
def test_config_stream_from_declaration():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=STREAMING_CHAIN_YAML))
|
||||
def test_config_stream_from_yaml():
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(STREAMING_CHAIN_YAML))
|
||||
streaming = flow.kickoff()
|
||||
assert isinstance(streaming, FlowStreamingOutput)
|
||||
for _ in streaming:
|
||||
@@ -2464,7 +2448,7 @@ def test_config_stream_from_declaration():
|
||||
assert flow.stream is True
|
||||
|
||||
|
||||
def test_config_defer_trace_finalization_from_declaration():
|
||||
def test_config_defer_trace_finalization_from_yaml():
|
||||
_, _, baseline_events = _run_capturing_flow_lifecycle(
|
||||
CHAIN_YAML, [FlowFinishedEvent]
|
||||
)
|
||||
@@ -2478,7 +2462,7 @@ def test_config_defer_trace_finalization_from_declaration():
|
||||
assert deferred_events == []
|
||||
|
||||
|
||||
def test_config_checkpoint_from_declaration(tmp_path):
|
||||
def test_config_checkpoint_from_yaml(tmp_path):
|
||||
yaml_str = (
|
||||
CHAIN_YAML
|
||||
+ f"""
|
||||
@@ -2487,23 +2471,19 @@ config:
|
||||
location: {tmp_path}
|
||||
"""
|
||||
)
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
assert isinstance(flow.checkpoint, CheckpointConfig)
|
||||
assert flow.checkpoint.location == str(tmp_path)
|
||||
|
||||
|
||||
def test_config_input_provider_from_declaration():
|
||||
flow = Flow.from_definition(
|
||||
FlowDefinition.from_declaration(contents=INPUT_PROVIDER_CHAIN_YAML)
|
||||
)
|
||||
def test_config_input_provider_from_yaml():
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(INPUT_PROVIDER_CHAIN_YAML))
|
||||
assert isinstance(flow.input_provider, StubInputProvider)
|
||||
|
||||
|
||||
def test_round_trip_config_equivalence():
|
||||
class_flow = ConfiguredFlow()
|
||||
definition = FlowDefinition.from_declaration(
|
||||
contents=ConfiguredFlow.flow_definition().to_yaml()
|
||||
)
|
||||
definition = FlowDefinition.from_yaml(ConfiguredFlow.flow_definition().to_yaml())
|
||||
definition_flow = Flow.from_definition(definition)
|
||||
|
||||
assert definition.config.suppress_flow_events is True
|
||||
@@ -2673,9 +2653,9 @@ class MethodPersistedFlow(Flow):
|
||||
return "two"
|
||||
|
||||
|
||||
def test_flow_level_persist_from_declaration_saves_once_per_method():
|
||||
def test_flow_level_persist_from_yaml_saves_once_per_method():
|
||||
yaml_str = _flow_level_persist_yaml("yaml-flow-level")
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "two"
|
||||
@@ -2685,9 +2665,9 @@ def test_flow_level_persist_from_declaration_saves_once_per_method():
|
||||
assert final_save["id"] == flow.state["id"]
|
||||
|
||||
|
||||
def test_method_level_persist_from_declaration_saves_only_that_method():
|
||||
def test_method_level_persist_from_yaml_saves_only_that_method():
|
||||
yaml_str = _method_level_persist_yaml("yaml-method-level")
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
flow.kickoff()
|
||||
|
||||
assert _saved_methods("yaml-method-level") == ["first"]
|
||||
@@ -2716,20 +2696,20 @@ methods:
|
||||
persist:
|
||||
enabled: false
|
||||
"""
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
flow.kickoff()
|
||||
|
||||
assert _saved_methods("yaml-opt-out") == ["first"]
|
||||
|
||||
|
||||
def test_persist_restore_by_id_from_declaration():
|
||||
def test_persist_restore_by_id_from_yaml():
|
||||
yaml_str = _flow_level_persist_yaml("yaml-restore")
|
||||
|
||||
flow1 = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow1 = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
flow1.kickoff()
|
||||
assert flow1.state["count"] == 2
|
||||
|
||||
flow2 = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow2 = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
flow2.kickoff(inputs={"id": flow1.state["id"]})
|
||||
assert flow2.state["count"] == 4
|
||||
|
||||
@@ -2749,9 +2729,7 @@ def test_method_level_persist_decorator_saves_only_that_method():
|
||||
|
||||
|
||||
def test_round_trip_persist_equivalence():
|
||||
definition = FlowDefinition.from_declaration(
|
||||
contents=ClassPersistedFlow.flow_definition().to_yaml()
|
||||
)
|
||||
definition = FlowDefinition.from_yaml(ClassPersistedFlow.flow_definition().to_yaml())
|
||||
|
||||
before = len(DefinitionStoreBackend.saves["class-decorator"])
|
||||
flow = Flow.from_definition(definition)
|
||||
@@ -2760,7 +2738,7 @@ def test_round_trip_persist_equivalence():
|
||||
assert _saved_methods("class-decorator")[before:] == ["first", "second"]
|
||||
|
||||
|
||||
def test_method_persist_backend_overrides_flow_level_backend_from_declaration():
|
||||
def test_method_persist_backend_overrides_flow_level_backend_from_yaml():
|
||||
yaml_str = f"""
|
||||
schema: crewai.flow/v1
|
||||
name: PersistedFlow
|
||||
@@ -2784,7 +2762,7 @@ methods:
|
||||
persistence_type: DefinitionStoreBackend
|
||||
store: yaml-mixed-method
|
||||
"""
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
flow.kickoff()
|
||||
|
||||
assert _saved_methods("yaml-mixed-flow") == ["first"]
|
||||
@@ -2932,8 +2910,8 @@ methods:
|
||||
"""
|
||||
|
||||
|
||||
def test_human_feedback_from_declaration_default_outcome_routes():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=REVIEW_YAML))
|
||||
def test_human_feedback_from_yaml_default_outcome_routes():
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(REVIEW_YAML))
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="") as request:
|
||||
result = flow.kickoff()
|
||||
@@ -2944,8 +2922,8 @@ def test_human_feedback_from_declaration_default_outcome_routes():
|
||||
assert flow.last_human_feedback.output == "draft-content"
|
||||
|
||||
|
||||
def test_human_feedback_from_declaration_collapses_and_routes():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=REVIEW_YAML))
|
||||
def test_human_feedback_from_yaml_collapses_and_routes():
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(REVIEW_YAML))
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="ship it"),
|
||||
@@ -2962,7 +2940,7 @@ def test_round_trip_human_feedback_equivalence():
|
||||
with patch.object(class_flow, "_request_human_feedback", return_value=""):
|
||||
class_result = class_flow.kickoff()
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=ReviewFlow.flow_definition().to_yaml())
|
||||
definition = FlowDefinition.from_yaml(ReviewFlow.flow_definition().to_yaml())
|
||||
twin = Flow.from_definition(definition)
|
||||
with patch.object(twin, "_request_human_feedback", return_value=""):
|
||||
twin_result = twin.kickoff()
|
||||
@@ -2975,8 +2953,8 @@ def test_round_trip_human_feedback_equivalence():
|
||||
)
|
||||
|
||||
|
||||
def test_human_feedback_pending_and_resume_from_declaration():
|
||||
definition = FlowDefinition.from_declaration(contents=PENDING_REVIEW_YAML)
|
||||
def test_human_feedback_pending_and_resume_from_yaml():
|
||||
definition = FlowDefinition.from_yaml(PENDING_REVIEW_YAML)
|
||||
|
||||
flow = Flow.from_definition(definition)
|
||||
pending = flow.kickoff()
|
||||
@@ -2997,7 +2975,7 @@ def test_human_feedback_pending_and_resume_from_declaration():
|
||||
assert flow_id not in DefinitionStoreBackend.pending
|
||||
|
||||
|
||||
def test_flow_config_provider_fallback_from_declaration():
|
||||
def test_flow_config_provider_fallback_from_yaml():
|
||||
yaml_str = f"""
|
||||
schema: crewai.flow/v1
|
||||
name: ConfigProviderFlow
|
||||
@@ -3023,7 +3001,7 @@ methods:
|
||||
return "from-config"
|
||||
|
||||
provider = RecordingProvider()
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
previous = flow_config.hitl_provider
|
||||
flow_config.hitl_provider = provider
|
||||
@@ -3126,7 +3104,7 @@ methods:
|
||||
message: "Review:"
|
||||
provider: {__name__}:_NeedsArgsProvider
|
||||
"""
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="cannot instantiate human_feedback.provider ref"
|
||||
@@ -3147,7 +3125,7 @@ methods:
|
||||
message: "Review:"
|
||||
provider: missing_module_xyz:Provider
|
||||
"""
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str))
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="unresolvable human_feedback.provider ref"
|
||||
@@ -3159,7 +3137,7 @@ def _checkpoint_chain_flow(tmp_path):
|
||||
from crewai.state.provider.json_provider import JsonProvider
|
||||
from crewai.state.runtime import RuntimeState
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=CHAIN_YAML)
|
||||
definition = FlowDefinition.from_yaml(CHAIN_YAML)
|
||||
flow = Flow.from_definition(definition)
|
||||
result = flow.kickoff()
|
||||
assert result == "confirmed:True"
|
||||
@@ -3199,7 +3177,7 @@ state:
|
||||
methods: {}
|
||||
"""
|
||||
with pytest.raises(ValidationError, match="default"):
|
||||
FlowDefinition.from_declaration(contents=yaml_str)
|
||||
FlowDefinition.from_yaml(yaml_str)
|
||||
|
||||
|
||||
def test_definition_method_missing_from_class_fails_loudly():
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.14.8a4"
|
||||
__version__ = "1.14.8a2"
|
||||
|
||||
63
uv.lock
generated
63
uv.lock
generated
@@ -13,7 +13,7 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-06-20T16:46:21.117658Z"
|
||||
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
@@ -1452,9 +1452,9 @@ requires-dist = [
|
||||
{ name = "openai", specifier = ">=2.30.0,<3" },
|
||||
{ name = "openpyxl", specifier = "~=3.1.5" },
|
||||
{ name = "openpyxl", marker = "extra == 'openpyxl'", specifier = "~=3.1.5" },
|
||||
{ name = "opentelemetry-api", specifier = "~=1.42.0" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.42.0" },
|
||||
{ name = "opentelemetry-sdk", specifier = "~=1.42.0" },
|
||||
{ name = "opentelemetry-api", specifier = "~=1.34.0" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.34.0" },
|
||||
{ name = "opentelemetry-sdk", specifier = "~=1.34.0" },
|
||||
{ name = "pandas", marker = "extra == 'pandas'", specifier = "~=2.2.3" },
|
||||
{ name = "pdfplumber", specifier = "~=0.11.4" },
|
||||
{ name = "portalocker", specifier = "~=2.7.0" },
|
||||
@@ -1539,9 +1539,9 @@ requires-dist = [
|
||||
{ name = "appdirs", specifier = "~=1.4.4" },
|
||||
{ name = "cryptography", specifier = ">=42.0" },
|
||||
{ name = "httpx", specifier = "~=0.28.1" },
|
||||
{ name = "opentelemetry-api", specifier = "~=1.42.0" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.42.0" },
|
||||
{ name = "opentelemetry-sdk", specifier = "~=1.42.0" },
|
||||
{ name = "opentelemetry-api", specifier = "~=1.34.0" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.34.0" },
|
||||
{ name = "opentelemetry-sdk", specifier = "~=1.34.0" },
|
||||
{ name = "packaging", specifier = ">=23.0" },
|
||||
{ name = "portalocker", specifier = "~=2.7.0" },
|
||||
{ name = "pydantic", specifier = ">=2.11.9,<2.13" },
|
||||
@@ -5585,44 +5585,45 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.42.1"
|
||||
version = "1.34.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/5e/94a8cb759e4e409022229418294e098ca7feca00eb3c467bb20cbd329bda/opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3", size = 64987, upload-time = "2025-06-10T08:55:19.818Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/3a/2ba85557e8dc024c0842ad22c570418dc02c36cbd1ab4b832a93edf071b8/opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c", size = 65767, upload-time = "2025-06-10T08:54:56.717Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp"
|
||||
version = "1.42.1"
|
||||
version = "1.34.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/94/8637919a5d01f81dacf510234bc0110b944f4687a6e96b0a02adf2f6bdce/opentelemetry_exporter_otlp-1.42.1.tar.gz", hash = "sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9", size = 6086, upload-time = "2026-05-21T16:32:51.963Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/ba/786b4de7e39d88043622d901b92c4485835f43e0be76c2824d2687911bc2/opentelemetry_exporter_otlp-1.34.1.tar.gz", hash = "sha256:71c9ad342d665d9e4235898d205db17c5764cd7a69acb8a5dcd6d5e04c4c9988", size = 6173, upload-time = "2025-06-10T08:55:21.595Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/4d/c26080295a36fd22e201fefd7cb9c22cd203189b1af8cd73b158382b7ad8/opentelemetry_exporter_otlp-1.42.1-py3-none-any.whl", hash = "sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee", size = 6733, upload-time = "2026-05-21T16:32:31.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/c1/259b8d8391c968e8f005d8a0ccefcb41aeef64cf55905cd0c0db4e22aaee/opentelemetry_exporter_otlp-1.34.1-py3-none-any.whl", hash = "sha256:f4a453e9cde7f6362fd4a090d8acf7881d1dc585540c7b65cbd63e36644238d4", size = 7040, upload-time = "2025-06-10T08:54:59.655Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-common"
|
||||
version = "1.42.1"
|
||||
version = "1.34.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-proto" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/f0/ff235936ee40db93360233b62da932d4fd9e8d103cd090c6bcb9afaf5f01/opentelemetry_exporter_otlp_proto_common-1.34.1.tar.gz", hash = "sha256:b59a20a927facd5eac06edaf87a07e49f9e4a13db487b7d8a52b37cb87710f8b", size = 20817, upload-time = "2025-06-10T08:55:22.55Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/e8/8b292a11cc8d8d87ec0c4089ae21b6a58af49ca2e51fa916435bc922fdc7/opentelemetry_exporter_otlp_proto_common-1.34.1-py3-none-any.whl", hash = "sha256:8e2019284bf24d3deebbb6c59c71e6eef3307cd88eff8c633e061abba33f7e87", size = 18834, upload-time = "2025-06-10T08:55:00.806Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-grpc"
|
||||
version = "1.42.1"
|
||||
version = "1.34.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos" },
|
||||
@@ -5633,14 +5634,14 @@ dependencies = [
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/f7/bb63837a3edb9ca857aaf5760796874e7cecddc88a2571b0992865a48fb6/opentelemetry_exporter_otlp_proto_grpc-1.34.1.tar.gz", hash = "sha256:7c841b90caa3aafcfc4fee58487a6c71743c34c6dc1787089d8b0578bbd794dd", size = 22566, upload-time = "2025-06-10T08:55:23.214Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/42/0a4dd47e7ef54edf670c81fc06a83d68ea42727b82126a1df9dd0477695d/opentelemetry_exporter_otlp_proto_grpc-1.34.1-py3-none-any.whl", hash = "sha256:04bb8b732b02295be79f8a86a4ad28fae3d4ddb07307a98c7aa6f331de18cca6", size = 18615, upload-time = "2025-06-10T08:55:02.214Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-http"
|
||||
version = "1.42.1"
|
||||
version = "1.34.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos" },
|
||||
@@ -5651,48 +5652,48 @@ dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/8f/954bc725961cbe425a749d55c0ba1df46832a5999eae764d1a7349ac1c29/opentelemetry_exporter_otlp_proto_http-1.34.1.tar.gz", hash = "sha256:aaac36fdce46a8191e604dcf632e1f9380c7d5b356b27b3e0edb5610d9be28ad", size = 15351, upload-time = "2025-06-10T08:55:24.657Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/54/b05251c04e30c1ac70cf4a7c5653c085dfcf2c8b98af71661d6a252adc39/opentelemetry_exporter_otlp_proto_http-1.34.1-py3-none-any.whl", hash = "sha256:5251f00ca85872ce50d871f6d3cc89fe203b94c3c14c964bbdc3883366c705d8", size = 17744, upload-time = "2025-06-10T08:55:03.802Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-proto"
|
||||
version = "1.42.1"
|
||||
version = "1.34.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/b3/c3158dd012463bb7c0eb7304a85a6f63baeeb5b4c93a53845cf89f848c7e/opentelemetry_proto-1.34.1.tar.gz", hash = "sha256:16286214e405c211fc774187f3e4bbb1351290b8dfb88e8948af209ce85b719e", size = 34344, upload-time = "2025-06-10T08:55:32.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/ab/4591bfa54e946350ce8b3f28e5c658fe9785e7cd11e9c11b1671a867822b/opentelemetry_proto-1.34.1-py3-none-any.whl", hash = "sha256:eb4bb5ac27f2562df2d6857fc557b3a481b5e298bc04f94cc68041f00cebcbd2", size = 55692, upload-time = "2025-06-10T08:55:14.904Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.42.1"
|
||||
version = "1.34.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/41/fe20f9036433da8e0fcef568984da4c1d1c771fa072ecd1a4d98779dccdd/opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d", size = 159441, upload-time = "2025-06-10T08:55:33.028Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/1b/def4fe6aa73f483cabf4c748f4c25070d5f7604dcc8b52e962983491b29e/opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e", size = 118477, upload-time = "2025-06-10T08:55:16.02Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.63b1"
|
||||
version = "0.55b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/f0/f33458486da911f47c4aa6db9bda308bb80f3236c111bf848bd870c16b16/opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3", size = 119829, upload-time = "2025-06-10T08:55:33.881Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/89/267b0af1b1d0ba828f0e60642b6a5116ac1fd917cde7fc02821627029bd1/opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed", size = 196223, upload-time = "2025-06-10T08:55:17.638Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user