mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-03-28 14:48:17 +00:00
Compare commits
2 Commits
fix/window
...
devin/1774
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e18477235b | ||
|
|
a4890e5626 |
@@ -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",
|
||||
|
||||
@@ -6,7 +6,7 @@ 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",
|
||||
|
||||
@@ -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",
|
||||
@@ -43,8 +43,7 @@ dependencies = [
|
||||
"uv~=0.9.13",
|
||||
"aiosqlite~=0.21.0",
|
||||
"pyyaml~=6.0",
|
||||
"lancedb>=0.29.2; sys_platform != 'win32'",
|
||||
"qdrant-edge-py>=0.6.0; sys_platform == 'win32'",
|
||||
"lancedb>=0.29.2",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
import contextvars
|
||||
from datetime import datetime
|
||||
@@ -69,7 +68,7 @@ class Memory(BaseModel):
|
||||
description="LLM for analysis (model name or BaseLLM instance).",
|
||||
)
|
||||
storage: Annotated[StorageBackend | str, PlainValidator(_passthrough)] = Field(
|
||||
default="qdrant-edge" if sys.platform == "win32" else "lancedb",
|
||||
default="lancedb",
|
||||
description="Storage backend instance or path string.",
|
||||
)
|
||||
embedder: Any = Field(
|
||||
|
||||
@@ -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)
|
||||
|
||||
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,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