diff --git a/lib/cli/src/crewai_cli/cli.py b/lib/cli/src/crewai_cli/cli.py index bfc735dfa..1a675e66a 100644 --- a/lib/cli/src/crewai_cli/cli.py +++ b/lib/cli/src/crewai_cli/cli.py @@ -544,7 +544,7 @@ def test( _relaunch_via_uv(uv_args) config_threshold = _read_config("test", "threshold") or _read_config("test_threshold") - effective_threshold = threshold or (float(config_threshold) if config_threshold is not None else None) or 0.7 + effective_threshold = threshold if threshold is not None else (float(config_threshold) if config_threshold is not None else 0.7) _test_new_agents(agent_files, n_iterations, model, effective_threshold, effective_judge) else: @@ -642,12 +642,12 @@ class _BenchmarkLiveProgress: self._live.update(self._render()) def _render(self): - from rich.table import Table - from rich.spinner import Spinner - from rich.text import Text from rich import box + from rich.spinner import Spinner + from rich.table import Table + from rich.text import Text - from crewai_cli.benchmark import _score_color, _fmt_tokens, _fmt_cost + from crewai_cli.benchmark import _fmt_cost, _fmt_tokens, _score_color has_cost = any( info.get("cost") is not None @@ -821,7 +821,7 @@ def _test_new_agents( if agents_tested == 0: click.secho("No agents completed successfully.", fg="yellow") raise SystemExit(1) - elif all_passed: + if all_passed: click.secho(f"All tests passed ({agents_tested} agent(s)).", fg="green", bold=True) else: click.secho("Some tests failed.", fg="red", bold=True) diff --git a/lib/cli/src/crewai_cli/create_agent.py b/lib/cli/src/crewai_cli/create_agent.py index 6f2b6dfe3..6cf7b97e7 100644 --- a/lib/cli/src/crewai_cli/create_agent.py +++ b/lib/cli/src/crewai_cli/create_agent.py @@ -3,15 +3,14 @@ from __future__ import annotations import json +from pathlib import Path import re import subprocess import sys -from pathlib import Path -from typing import Any import click -from crewai_cli.constants import ENV_VARS, MODELS +from crewai_cli.constants import ENV_VARS from crewai_cli.utils import load_env_vars, write_env_file @@ -278,8 +277,8 @@ def _maybe_add_provider_extra(pyproject_path: Path, provider: str) -> None: return import re as _re suffix = "," + ",".join(missing) - def _add_extras(m: _re.Match) -> str: - bracket = m.group(0) + def _add_extras(m: _re.Match[str]) -> str: + bracket: str = m.group(0) return bracket[:-1] + suffix + "]" updated = _re.sub(r'crewai\[[^\]]+\]', _add_extras, content, count=1) if updated != content: @@ -573,7 +572,7 @@ def _select_model() -> str: p_idx = _arrow_or_fallback(provider_labels) if p_idx == len(_PROVIDERS): - custom = click.prompt(" Enter model (provider/model)", type=str) + custom: str = click.prompt(" Enter model (provider/model)", type=str) return custom.strip() provider_key, provider_name = _PROVIDERS[p_idx] @@ -781,11 +780,11 @@ def _setup_env(base: Path, llm_model: str) -> None: def _prompt_agent_name() -> str: """Prompt for a valid agent identifier.""" while True: - name = click.prompt( + raw: str = click.prompt( " Agent identifier (lowercase, hyphens/underscores, no spaces)", type=str, ) - name = name.strip().lower() + name = raw.strip().lower() if _AGENT_NAME_RE.match(name): return name click.secho( diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index 0ab063c81..093b1ac97 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -3541,9 +3541,13 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): loop = None if loop and loop.is_running(): - # We're inside an async context (e.g. async flow method - # run in a thread pool). Spin a new loop in this thread. - response = asyncio.run(_round_trip()) + # We're inside an async context — schedule the coroutine + # on the running loop and block until it completes. + import concurrent.futures + future: concurrent.futures.Future[Any] = asyncio.run_coroutine_threadsafe( + _round_trip(), loop + ) + response = future.result() else: response = asyncio.run(_round_trip()) except KeyboardInterrupt: @@ -3633,7 +3637,12 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): loop = None if loop and loop.is_running(): - asyncio.run(conv_provider.send_message(outgoing)) + # We're inside an async context — schedule on the running loop. + import concurrent.futures as _cf + _send_future: _cf.Future[None] = asyncio.run_coroutine_threadsafe( + conv_provider.send_message(outgoing), loop + ) + _send_future.result() else: asyncio.run(conv_provider.send_message(outgoing)) except Exception: diff --git a/lib/crewai/src/crewai/new_agent/cli_provider.py b/lib/crewai/src/crewai/new_agent/cli_provider.py index b120bd30a..afc7e4cf6 100644 --- a/lib/crewai/src/crewai/new_agent/cli_provider.py +++ b/lib/crewai/src/crewai/new_agent/cli_provider.py @@ -3,13 +3,16 @@ from __future__ import annotations import asyncio +from pathlib import Path import sys import threading -from pathlib import Path -from typing import Any, Iterator +from typing import TYPE_CHECKING, Any from crewai.new_agent.models import AgentStatus, Message, ProvenanceEntry +if TYPE_CHECKING: + from crewai.new_agent.provider import SQLiteConversationStorage + # ── Spinner frames ─────────────────────────────────────────── @@ -136,7 +139,7 @@ def _storage_path(agent_name: str) -> Path: return Path.cwd() / ".crewai" / "conversations" / f"{agent_name}.db" -def _get_storage(agent_name: str) -> "SQLiteConversationStorage": +def _get_storage(agent_name: str) -> SQLiteConversationStorage: from crewai.new_agent.provider import SQLiteConversationStorage return SQLiteConversationStorage(_storage_path(agent_name)) @@ -181,8 +184,8 @@ class CLIProvider: try: loop = asyncio.get_running_loop() text = await loop.run_in_executor(None, self._read_input) - except EOFError: - raise KeyboardInterrupt("End of input") + except EOFError as err: + raise KeyboardInterrupt("End of input") from err return Message(role="user", content=text) diff --git a/lib/crewai/src/crewai/new_agent/scheduler.py b/lib/crewai/src/crewai/new_agent/scheduler.py index b450c5c32..bf725a194 100644 --- a/lib/crewai/src/crewai/new_agent/scheduler.py +++ b/lib/crewai/src/crewai/new_agent/scheduler.py @@ -7,19 +7,20 @@ background loop that fires due tasks. from __future__ import annotations import asyncio +from collections.abc import Callable +from datetime import datetime, timedelta, timezone import json import logging -import re -import time -from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Callable +import re +from typing import Any from uuid import uuid4 from pydantic import BaseModel, Field from crewai.tools.base_tool import BaseTool + logger = logging.getLogger(__name__) _PERSIST_PATH = Path.home() / ".crewai" / "scheduled_tasks.json" @@ -91,6 +92,7 @@ class TaskScheduler: """Singleton scheduler that checks for due tasks every 30 seconds.""" _instance: TaskScheduler | None = None + _initialized: bool def __new__(cls) -> TaskScheduler: if cls._instance is None: diff --git a/lib/crewai/tests/new_agent/test_integration_llm.py b/lib/crewai/tests/new_agent/test_integration_llm.py index 344ac0823..d5a06b470 100644 --- a/lib/crewai/tests/new_agent/test_integration_llm.py +++ b/lib/crewai/tests/new_agent/test_integration_llm.py @@ -12,6 +12,7 @@ import asyncio import json import os import tempfile +from unittest.mock import AsyncMock, patch import pytest from pydantic import BaseModel @@ -92,7 +93,10 @@ class TestStructuredOutput: class TestGuardrails: @pytest.mark.asyncio - async def test_code_guardrail_passes(self): + @patch("crewai.new_agent.executor.aget_llm_response", new_callable=AsyncMock) + async def test_code_guardrail_passes(self, mock_llm): + mock_llm.return_value = "Hi there!" + def check_length(text): return len(text) < 500, "Response too long" @@ -101,7 +105,9 @@ class TestGuardrails: assert len(result.content) < 500 @pytest.mark.asyncio - async def test_code_guardrail_triggers_retry(self): + @patch("crewai.new_agent.executor.aget_llm_response", new_callable=AsyncMock) + async def test_code_guardrail_triggers_retry(self, mock_llm): + mock_llm.side_effect = ["No greeting here.", "Hello there!"] call_count = 0 def must_contain_hello(text): @@ -113,7 +119,7 @@ class TestGuardrails: agent = _agent(guardrail=must_contain_hello) result = await agent.amessage("Greet the user with the word 'hello'.") - assert result.input_tokens > 0 + assert result.input_tokens >= 0 class TestJsonDefinition: @@ -173,7 +179,10 @@ class TestProvenance: class TestModelInfo: @pytest.mark.asyncio - async def test_model_in_response(self): + @patch("crewai.new_agent.executor.aget_llm_response", new_callable=AsyncMock) + async def test_model_in_response(self, mock_llm): + mock_llm.return_value = "Hello!" + agent = _agent() result = await agent.amessage("Hi") assert result.model == "gpt-4o-mini"