mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-22 09:28:12 +00:00
Compare commits
5 Commits
docs/file-
...
devin/1774
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e18477235b | ||
|
|
a4890e5626 | ||
|
|
6193e082e1 | ||
|
|
33f33c6fcc | ||
|
|
74976b157d |
@@ -4,6 +4,29 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.2
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة مرحلة إصدار المؤسسات إلى إصدار أدوات المطورين
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- الحفاظ على قيمة إرجاع الطريقة كإخراج تدفق لـ @human_feedback مع emit
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.1
|
||||
- مراجعة سياسة الأمان وتعليمات الإبلاغ
|
||||
|
||||
## المساهمون
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.1
|
||||
|
||||
|
||||
1863
docs/docs.json
1863
docs/docs.json
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,29 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add enterprise release phase to devtools release
|
||||
|
||||
### Bug Fixes
|
||||
- Preserve method return value as flow output for @human_feedback with emit
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.1
|
||||
- Revise security policy and reporting instructions
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.1
|
||||
|
||||
|
||||
@@ -134,29 +134,6 @@ result = flow.kickoff(
|
||||
)
|
||||
```
|
||||
|
||||
You can also define file types directly in your flow state for structured file handling:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from crewai.flow.flow import Flow, start
|
||||
from crewai_files import ImageFile, PDFFile
|
||||
|
||||
class DocumentState(BaseModel):
|
||||
document: PDFFile
|
||||
cover_image: ImageFile
|
||||
title: str = ""
|
||||
|
||||
class DocumentFlow(Flow[DocumentState]):
|
||||
@start()
|
||||
def process(self):
|
||||
content = self.state.document.read()
|
||||
return {"processed": True}
|
||||
```
|
||||
|
||||
<Note type="info" title="CrewAI Platform Integration">
|
||||
When deploying flows to the CrewAI Platform (AMP), file fields in your state automatically render as file upload dropzones in the UI. For API usage, you can pass URL strings directly and Pydantic coerces them to file objects automatically. See [Flows - File Inputs](/en/concepts/flows#file-inputs) for details.
|
||||
</Note>
|
||||
|
||||
### With Standalone Agents
|
||||
|
||||
Pass files directly to agent kickoff:
|
||||
|
||||
@@ -341,69 +341,6 @@ flow.kickoff()
|
||||
|
||||
By providing both unstructured and structured state management options, CrewAI Flows empowers developers to build AI workflows that are both flexible and robust, catering to a wide range of application requirements.
|
||||
|
||||
## File Inputs
|
||||
|
||||
Flows support file inputs through the `crewai-files` package, enabling you to build workflows that process images, PDFs, and other file types. When you use file types like `ImageFile` or `PDFFile` in your flow state, they integrate seamlessly with both local development and the CrewAI Platform.
|
||||
|
||||
<Note type="info" title="Optional Dependency">
|
||||
File support requires the optional `crewai-files` package. Install it with:
|
||||
|
||||
```bash
|
||||
uv add 'crewai[file-processing]'
|
||||
```
|
||||
</Note>
|
||||
|
||||
### Using File Types in Flow State
|
||||
|
||||
You can include file types directly in your structured flow state:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from crewai.flow.flow import Flow, start
|
||||
from crewai_files import ImageFile, PDFFile
|
||||
|
||||
class DocumentProcessingState(BaseModel):
|
||||
document: PDFFile # Renders as file upload in CrewAI Platform
|
||||
cover_image: ImageFile # Renders as image upload
|
||||
title: str = "" # Renders as text input
|
||||
|
||||
class DocumentFlow(Flow[DocumentProcessingState]):
|
||||
@start()
|
||||
def process_document(self):
|
||||
# Access the file - works with URLs, paths, or uploaded files
|
||||
content = self.state.document.read()
|
||||
# Or pass to an agent with VisionTool, etc.
|
||||
return {"processed": True}
|
||||
```
|
||||
|
||||
### CrewAI Platform Integration
|
||||
|
||||
When you deploy a flow to the CrewAI Platform (AMP), file fields in your state automatically render as file upload dropzones in the UI. This makes it easy to build user-facing applications that accept file uploads without any additional frontend work.
|
||||
|
||||
| State Field Type | Platform UI Rendering |
|
||||
|:-----------------|:----------------------|
|
||||
| `ImageFile` | Image upload dropzone |
|
||||
| `PDFFile` | PDF upload dropzone |
|
||||
| `AudioFile` | Audio upload dropzone |
|
||||
| `VideoFile` | Video upload dropzone |
|
||||
| `TextFile` | Text file upload dropzone |
|
||||
| `str`, `int`, etc. | Standard form inputs |
|
||||
|
||||
### API Usage
|
||||
|
||||
When calling your flow via API, you can pass URL strings directly for file fields. Pydantic automatically coerces URLs into the appropriate file type:
|
||||
|
||||
```python
|
||||
# API request body - URLs are automatically converted to file objects
|
||||
{
|
||||
"document": "https://example.com/report.pdf",
|
||||
"cover_image": "https://example.com/cover.png",
|
||||
"title": "Q4 Report"
|
||||
}
|
||||
```
|
||||
|
||||
For more details on file types, sources, and provider support, see the [Files documentation](/en/concepts/files).
|
||||
|
||||
## Flow Persistence
|
||||
|
||||
The @persist decorator enables automatic state persistence in CrewAI Flows, allowing you to maintain flow state across restarts or different workflow executions. This decorator can be applied at either the class level or method level, providing flexibility in how you manage state persistence.
|
||||
|
||||
@@ -4,6 +4,29 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- devtools 릴리스에 기업 릴리스 단계 추가
|
||||
|
||||
### 버그 수정
|
||||
- @human_feedback과 함께 emit을 사용할 때 메서드 반환 값을 흐름 출력으로 유지
|
||||
|
||||
### 문서
|
||||
- v1.12.1에 대한 변경 로그 및 버전 업데이트
|
||||
- 보안 정책 및 보고 지침 수정
|
||||
|
||||
## 기여자
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.1
|
||||
|
||||
|
||||
@@ -4,6 +4,29 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar fase de lançamento empresarial ao lançamento do devtools
|
||||
|
||||
### Correções de Bugs
|
||||
- Preservar o valor de retorno do método como saída de fluxo para @human_feedback com emit
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.1
|
||||
- Revisar política de segurança e instruções de relatório
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Greyson LaLonde", email = "greyson@crewai.com" }
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
requires-python = ">=3.10, <3.15"
|
||||
dependencies = [
|
||||
"Pillow~=12.1.1",
|
||||
"pypdf~=6.9.1",
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.12.1"
|
||||
__version__ = "1.12.2"
|
||||
|
||||
@@ -6,12 +6,12 @@ readme = "README.md"
|
||||
authors = [
|
||||
{ name = "João Moura", email = "joaomdmoura@gmail.com" },
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
requires-python = ">=3.10, <3.15"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.12.1",
|
||||
"crewai==1.12.2",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -309,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.12.1"
|
||||
__version__ = "1.12.2"
|
||||
|
||||
@@ -47,7 +47,7 @@ class BrowserSessionManager:
|
||||
Returns:
|
||||
An async browser instance specific to the thread
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
while True:
|
||||
with self._lock:
|
||||
if thread_id in self._async_sessions:
|
||||
|
||||
@@ -94,11 +94,9 @@ class BrowserBaseTool(BaseTool):
|
||||
try:
|
||||
import nest_asyncio # type: ignore[import-untyped]
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
nest_asyncio.apply(loop)
|
||||
result: str = asyncio.get_event_loop().run_until_complete(
|
||||
self._arun(*args, **kwargs)
|
||||
)
|
||||
result: str = loop.run_until_complete(self._arun(*args, **kwargs))
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error in patched _run: {e!s}"
|
||||
@@ -118,7 +116,7 @@ class BrowserBaseTool(BaseTool):
|
||||
def _is_in_asyncio_loop(self) -> bool:
|
||||
"""Check if we're currently in an asyncio event loop."""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
return loop.is_running()
|
||||
except RuntimeError:
|
||||
return False
|
||||
@@ -544,14 +542,13 @@ class BrowserToolkit:
|
||||
def _nest_current_loop(self) -> None:
|
||||
"""Apply nest_asyncio if we're in an asyncio loop."""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
try:
|
||||
import nest_asyncio
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
import nest_asyncio
|
||||
|
||||
nest_asyncio.apply(loop)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to apply nest_asyncio: {e!s}")
|
||||
nest_asyncio.apply(loop)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to apply nest_asyncio: {e!s}")
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ class SnowflakeSearchTool(BaseTool):
|
||||
with self._pool_lock:
|
||||
if self._connection_pool:
|
||||
return self._connection_pool.pop()
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
self._thread_pool, self._create_connection
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Joao Moura", email = "joao@crewai.com" }
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
requires-python = ">=3.10, <3.15"
|
||||
dependencies = [
|
||||
# Core Dependencies
|
||||
"pydantic~=2.11.9",
|
||||
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.12.1",
|
||||
"crewai-tools==1.12.2",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -42,7 +42,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.12.1"
|
||||
__version__ = "1.12.2"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ class MemoryTUI(App[None]):
|
||||
panel.loading = True
|
||||
try:
|
||||
scope = self._selected_scope if self._selected_scope != "/" else None
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
matches = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._memory.recall(query, scope=scope, limit=10, depth="deep"),
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.12.1"
|
||||
"crewai[tools]==1.12.2"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.12.1"
|
||||
"crewai[tools]==1.12.2"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.12.1"
|
||||
"crewai[tools]==1.12.2"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -883,7 +883,9 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self.human_feedback_history: list[HumanFeedbackResult] = []
|
||||
self.last_human_feedback: HumanFeedbackResult | None = None
|
||||
self._pending_feedback_context: PendingFeedbackContext | None = None
|
||||
self._human_feedback_method_output: Any = None # Stashed real output from @human_feedback with emit
|
||||
# Per-method stash for real @human_feedback output (keyed by method name)
|
||||
# Used to decouple routing outcome from method return value when emit is set
|
||||
self._human_feedback_method_outputs: dict[str, Any] = {}
|
||||
self.suppress_flow_events: bool = suppress_flow_events
|
||||
|
||||
# User input history (for self.ask())
|
||||
@@ -2295,10 +2297,12 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
# For @human_feedback methods with emit, the result is the collapsed outcome
|
||||
# (e.g., "approved") used for routing. But we want the actual method output
|
||||
# to be the stored result (for final flow output). Replace the last entry
|
||||
# if a stashed output exists.
|
||||
if self._human_feedback_method_output is not None:
|
||||
self._method_outputs[-1] = self._human_feedback_method_output
|
||||
self._human_feedback_method_output = None
|
||||
# if a stashed output exists. Dict-based stash is concurrency-safe and
|
||||
# handles None return values (presence in dict = stashed, not value).
|
||||
if method_name in self._human_feedback_method_outputs:
|
||||
self._method_outputs[-1] = self._human_feedback_method_outputs.pop(
|
||||
method_name
|
||||
)
|
||||
|
||||
self._method_execution_counts[method_name] = (
|
||||
self._method_execution_counts.get(method_name, 0) + 1
|
||||
|
||||
@@ -594,8 +594,9 @@ def human_feedback(
|
||||
# Stash the real method output for final flow result when emit is set
|
||||
# (result is the collapsed outcome string for routing, but we want to
|
||||
# preserve the actual method output as the flow's final result)
|
||||
# Uses per-method dict for concurrency safety and to handle None returns
|
||||
if emit:
|
||||
self._human_feedback_method_output = method_output
|
||||
self._human_feedback_method_outputs[func.__name__] = method_output
|
||||
|
||||
return result
|
||||
|
||||
@@ -624,8 +625,9 @@ def human_feedback(
|
||||
# Stash the real method output for final flow result when emit is set
|
||||
# (result is the collapsed outcome string for routing, but we want to
|
||||
# preserve the actual method output as the flow's final result)
|
||||
# Uses per-method dict for concurrency safety and to handle None returns
|
||||
if emit:
|
||||
self._human_feedback_method_output = method_output
|
||||
self._human_feedback_method_outputs[func.__name__] = method_output
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class ChromaDBClient(BaseClient):
|
||||
yield
|
||||
return
|
||||
lock_cm = store_lock(self._lock_name)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, lock_cm.__enter__)
|
||||
try:
|
||||
yield
|
||||
|
||||
@@ -266,7 +266,7 @@ class CrewStructuredTool:
|
||||
# Run sync functions in a thread pool
|
||||
import asyncio
|
||||
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
None, lambda: self.func(**parsed_args, **kwargs)
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -184,7 +184,7 @@ def create_streaming_state(
|
||||
|
||||
if use_async:
|
||||
async_queue = asyncio.Queue()
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
handler = _create_stream_handler(current_task_info, sync_queue, async_queue, loop)
|
||||
crewai_event_bus.register_handler(LLMStreamChunkEvent, handler)
|
||||
|
||||
@@ -726,3 +726,31 @@ class TestHumanFeedbackFinalOutputPreservation:
|
||||
# _method_outputs should contain the real output
|
||||
assert len(flow._method_outputs) == 1
|
||||
assert flow._method_outputs[0] == {"data": "real output"}
|
||||
|
||||
@patch("builtins.input", return_value="looks good")
|
||||
@patch("builtins.print")
|
||||
def test_none_return_value_is_preserved(self, mock_print, mock_input):
|
||||
"""A method returning None should preserve None as flow output, not the outcome string."""
|
||||
|
||||
class NoneReturnFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def process(self):
|
||||
# Method does work but returns None (implicit)
|
||||
pass
|
||||
|
||||
flow = NoneReturnFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value=""),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
# Final output should be None (the method's real return), not "approved"
|
||||
assert result is None, f"Expected None, got {result!r}"
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
210
lib/crewai/tests/test_python314_compat.py
Normal file
210
lib/crewai/tests/test_python314_compat.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""Tests for Python 3.14 compatibility.
|
||||
|
||||
Python 3.14 changed asyncio.get_event_loop() to raise RuntimeError when no
|
||||
running event loop exists instead of creating one. All async code paths must
|
||||
use asyncio.get_running_loop() instead.
|
||||
|
||||
See: https://github.com/crewAIInc/crewAI/issues/5109
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from crewai.utilities.streaming import create_streaming_state
|
||||
|
||||
|
||||
class TestStructuredToolAsyncCompat:
|
||||
"""Test that CrewStructuredTool.ainvoke uses get_running_loop correctly."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ainvoke_sync_func_uses_running_loop(self) -> None:
|
||||
"""ainvoke() with a sync function must use the running event loop."""
|
||||
|
||||
def sync_func(x: int) -> int:
|
||||
"""A sync function."""
|
||||
return x * 2
|
||||
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=sync_func, name="double", description="Doubles a number"
|
||||
)
|
||||
result = await tool.ainvoke({"x": 5})
|
||||
assert result == 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ainvoke_async_func(self) -> None:
|
||||
"""ainvoke() with an async function should call it directly."""
|
||||
|
||||
async def async_func(x: int) -> int:
|
||||
"""An async function."""
|
||||
return x * 3
|
||||
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=async_func, name="triple", description="Triples a number"
|
||||
)
|
||||
result = await tool.ainvoke({"x": 4})
|
||||
assert result == 12
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ainvoke_sync_func_runs_in_executor(self) -> None:
|
||||
"""Verify ainvoke offloads sync functions to an executor via the running loop."""
|
||||
import threading
|
||||
|
||||
call_thread_ids: list[int] = []
|
||||
|
||||
def sync_func(x: int) -> int:
|
||||
"""A sync function that records its thread."""
|
||||
call_thread_ids.append(threading.current_thread().ident or 0)
|
||||
return x + 1
|
||||
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=sync_func, name="inc", description="Increment"
|
||||
)
|
||||
|
||||
result = await tool.ainvoke({"x": 1})
|
||||
assert result == 2
|
||||
assert len(call_thread_ids) == 1
|
||||
# Sync func should run in a different thread (executor)
|
||||
assert call_thread_ids[0] != threading.current_thread().ident
|
||||
|
||||
|
||||
class TestStreamingStateAsyncCompat:
|
||||
"""Test that create_streaming_state uses get_running_loop correctly."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_streaming_state_async_uses_running_loop(self) -> None:
|
||||
"""create_streaming_state(use_async=True) must use the running loop."""
|
||||
task_info = {
|
||||
"index": 0,
|
||||
"name": "test",
|
||||
"id": "test-id",
|
||||
"agent_role": "tester",
|
||||
"agent_id": "agent-id",
|
||||
}
|
||||
state = create_streaming_state(
|
||||
current_task_info=task_info,
|
||||
result_holder=[],
|
||||
use_async=True,
|
||||
)
|
||||
assert state.loop is not None
|
||||
assert state.async_queue is not None
|
||||
assert state.loop is asyncio.get_running_loop()
|
||||
|
||||
def test_create_streaming_state_sync_no_loop_needed(self) -> None:
|
||||
"""create_streaming_state(use_async=False) should not require a loop."""
|
||||
task_info = {
|
||||
"index": 0,
|
||||
"name": "test",
|
||||
"id": "test-id",
|
||||
"agent_role": "tester",
|
||||
"agent_id": "agent-id",
|
||||
}
|
||||
state = create_streaming_state(
|
||||
current_task_info=task_info,
|
||||
result_holder=[],
|
||||
use_async=False,
|
||||
)
|
||||
assert state.loop is None
|
||||
assert state.async_queue is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_streaming_state_async_uses_get_running_loop_not_get_event_loop(
|
||||
self,
|
||||
) -> None:
|
||||
"""Verify create_streaming_state does not call asyncio.get_event_loop()."""
|
||||
task_info = {
|
||||
"index": 0,
|
||||
"name": "test",
|
||||
"id": "test-id",
|
||||
"agent_role": "tester",
|
||||
"agent_id": "agent-id",
|
||||
}
|
||||
|
||||
with patch("crewai.utilities.streaming.asyncio") as mock_asyncio:
|
||||
mock_asyncio.Queue = asyncio.Queue
|
||||
mock_asyncio.get_running_loop.return_value = asyncio.get_running_loop()
|
||||
|
||||
create_streaming_state(
|
||||
current_task_info=task_info,
|
||||
result_holder=[],
|
||||
use_async=True,
|
||||
)
|
||||
|
||||
mock_asyncio.get_running_loop.assert_called_once()
|
||||
mock_asyncio.get_event_loop.assert_not_called()
|
||||
|
||||
|
||||
class TestChromaDBClientAsyncCompat:
|
||||
"""Test that ChromaDBClient._alocked uses get_running_loop correctly."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alocked_without_lock_name(self) -> None:
|
||||
"""_alocked should yield immediately when no lock name is set."""
|
||||
from crewai.rag.chromadb.client import ChromaDBClient
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_ef = MagicMock()
|
||||
client = ChromaDBClient(
|
||||
client=mock_client,
|
||||
embedding_function=mock_ef,
|
||||
lock_name=None,
|
||||
)
|
||||
|
||||
async with client._alocked():
|
||||
pass # Should not raise
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alocked_uses_get_running_loop_not_get_event_loop(self) -> None:
|
||||
"""Verify _alocked does not call asyncio.get_event_loop()."""
|
||||
from crewai.rag.chromadb.client import ChromaDBClient
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_ef = MagicMock()
|
||||
client = ChromaDBClient(
|
||||
client=mock_client,
|
||||
embedding_function=mock_ef,
|
||||
lock_name="test-lock",
|
||||
)
|
||||
|
||||
with patch("crewai.rag.chromadb.client.asyncio") as mock_asyncio:
|
||||
loop = asyncio.get_running_loop()
|
||||
mock_asyncio.get_running_loop.return_value = loop
|
||||
|
||||
mock_cm = MagicMock()
|
||||
with patch("crewai.rag.chromadb.client.store_lock", return_value=mock_cm):
|
||||
async with client._alocked():
|
||||
pass
|
||||
|
||||
mock_asyncio.get_running_loop.assert_called()
|
||||
mock_asyncio.get_event_loop.assert_not_called()
|
||||
|
||||
|
||||
class TestGetRunningLoopInAsyncContext:
|
||||
"""General tests ensuring get_running_loop works in async contexts."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_running_loop_available_in_async_context(self) -> None:
|
||||
"""asyncio.get_running_loop() should work in an async context."""
|
||||
loop = asyncio.get_running_loop()
|
||||
assert loop is not None
|
||||
assert loop.is_running()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_in_executor_with_running_loop(self) -> None:
|
||||
"""run_in_executor should work with get_running_loop()."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def sync_work() -> str:
|
||||
return "done"
|
||||
|
||||
result = await loop.run_in_executor(None, sync_work)
|
||||
assert result == "done"
|
||||
|
||||
def test_get_running_loop_raises_outside_async(self) -> None:
|
||||
"""get_running_loop() should raise RuntimeError outside async context."""
|
||||
with pytest.raises(RuntimeError):
|
||||
asyncio.get_running_loop()
|
||||
@@ -271,7 +271,7 @@ async def test_mixed_sync_async_handler_execution():
|
||||
timeout=5
|
||||
)
|
||||
|
||||
await asyncio.get_event_loop().run_in_executor(None, wait_for_completion)
|
||||
await asyncio.get_running_loop().run_in_executor(None, wait_for_completion)
|
||||
|
||||
assert len(sync_executed) == 5
|
||||
assert len(async_executed) == 5
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.12.1"
|
||||
__version__ = "1.12.2"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "crewai-workspace"
|
||||
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
requires-python = ">=3.10,<3.15"
|
||||
authors = [
|
||||
{ name = "Joao Moura", email = "joao@crewai.com" }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user