diff --git a/conftest.py b/conftest.py index f50db10fd..dca182746 100644 --- a/conftest.py +++ b/conftest.py @@ -256,7 +256,8 @@ def vcr_cassette_dir(request: Any) -> str: for parent in test_file.parents: if ( - parent.name in ("crewai", "crewai-tools", "crewai-files", "cli") + parent.name + in ("crewai", "crewai-tools", "crewai-files", "cli", "crewai-core") and parent.parent.name == "lib" ): package_root = parent diff --git a/lib/cli/README.md b/lib/cli/README.md index 068f4b1c7..c72a718d1 100644 --- a/lib/cli/README.md +++ b/lib/cli/README.md @@ -1,8 +1,7 @@ # crewai-cli -CLI for CrewAI — scaffold, run, deploy and manage AI agent crews. - -The CLI depends on the `crewai` framework and pulls it in automatically. +CLI for CrewAI — scaffold, run, deploy and manage AI agent crews without +installing the full framework. ## Installation @@ -10,7 +9,17 @@ The CLI depends on the `crewai` framework and pulls it in automatically. pip install crewai-cli ``` -Or install via the framework's extra: +This pulls in `crewai-core` (shared utilities) but not the `crewai` framework +itself, so commands that don't need a crew loaded — `crewai version`, +`crewai login`, `crewai org list`, `crewai config *`, `crewai traces *`, +`crewai create`, `crewai template *` — work standalone. + +Commands that load a user's crew or flow (`crewai run`, `crewai train`, +`crewai test`, `crewai chat`, `crewai replay`, `crewai reset-memories`, +`crewai deploy push`, `crewai tool publish`) require `crewai` to be installed +in the project's environment. They print a clear error if it is missing. + +To install both at once: ```bash pip install crewai[cli] diff --git a/lib/cli/pyproject.toml b/lib/cli/pyproject.toml index 2261427e5..080104cf8 100644 --- a/lib/cli/pyproject.toml +++ b/lib/cli/pyproject.toml @@ -8,7 +8,7 @@ authors = [ ] requires-python = ">=3.10, <3.14" dependencies = [ - "crewai>=1.14.5a2", + "crewai-core>=1.14.5a2", "click~=8.1.7", "pydantic>=2.11.9,<2.13", "pydantic-settings~=2.10.1", @@ -22,7 +22,6 @@ dependencies = [ "packaging>=23.0", "python-dotenv>=1.2.2,<2", "uv~=0.9.13", - "portalocker~=2.7.0", ] [project.urls] diff --git a/lib/cli/src/crewai_cli/add_crew_to_flow.py b/lib/cli/src/crewai_cli/add_crew_to_flow.py index 38165e286..52d3d8e67 100644 --- a/lib/cli/src/crewai_cli/add_crew_to_flow.py +++ b/lib/cli/src/crewai_cli/add_crew_to_flow.py @@ -1,7 +1,7 @@ from pathlib import Path import click -from crewai.utilities.printer import PRINTER +from crewai_core.printer import PRINTER from crewai_cli.utils import copy_template diff --git a/lib/cli/src/crewai_cli/command.py b/lib/cli/src/crewai_cli/command.py index c33f1578a..229c76323 100644 --- a/lib/cli/src/crewai_cli/command.py +++ b/lib/cli/src/crewai_cli/command.py @@ -2,7 +2,7 @@ from __future__ import annotations import json -from crewai.telemetry.telemetry import Telemetry +from crewai_core.telemetry import Telemetry import httpx from rich.console import Console diff --git a/lib/cli/src/crewai_cli/create_flow.py b/lib/cli/src/crewai_cli/create_flow.py index a026cea44..75bd95ed2 100644 --- a/lib/cli/src/crewai_cli/create_flow.py +++ b/lib/cli/src/crewai_cli/create_flow.py @@ -2,7 +2,7 @@ from pathlib import Path import shutil import click -from crewai.telemetry import Telemetry +from crewai_core.telemetry import Telemetry def create_flow(name: str) -> None: diff --git a/lib/cli/src/crewai_cli/evaluate_crew.py b/lib/cli/src/crewai_cli/evaluate_crew.py index fe1967452..0c6138603 100644 --- a/lib/cli/src/crewai_cli/evaluate_crew.py +++ b/lib/cli/src/crewai_cli/evaluate_crew.py @@ -1,7 +1,7 @@ import subprocess import click -from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV +from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV from crewai_cli.utils import build_env_with_all_tool_credentials diff --git a/lib/cli/src/crewai_cli/replay_from_task.py b/lib/cli/src/crewai_cli/replay_from_task.py index 7f52e7090..76b90cf18 100644 --- a/lib/cli/src/crewai_cli/replay_from_task.py +++ b/lib/cli/src/crewai_cli/replay_from_task.py @@ -1,7 +1,7 @@ import subprocess import click -from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV +from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV from crewai_cli.utils import build_env_with_all_tool_credentials diff --git a/lib/cli/src/crewai_cli/run_crew.py b/lib/cli/src/crewai_cli/run_crew.py index fc9a4339c..dec85ca06 100644 --- a/lib/cli/src/crewai_cli/run_crew.py +++ b/lib/cli/src/crewai_cli/run_crew.py @@ -2,7 +2,7 @@ from enum import Enum import subprocess import click -from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV +from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV from packaging import version from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml diff --git a/lib/cli/src/crewai_cli/tools/main.py b/lib/cli/src/crewai_cli/tools/main.py index d1935d6ca..76de72c12 100644 --- a/lib/cli/src/crewai_cli/tools/main.py +++ b/lib/cli/src/crewai_cli/tools/main.py @@ -8,11 +8,6 @@ import tempfile from typing import Any import click -from crewai.events.listeners.tracing.utils import get_user_id -from crewai.utilities.project_utils import ( - extract_available_exports, - extract_tools_metadata, -) from rich.console import Console from crewai_cli import git @@ -33,6 +28,32 @@ from crewai_cli.utils import ( console = Console() +_REQUIRES_CREWAI_MSG = ( + "[red]This subcommand requires the full crewai package.\n" + "Install it with: pip install crewai[/red]" +) + + +def _require_project_utils() -> Any: + try: + from crewai.utilities import project_utils + + return project_utils + except ImportError: + console.print(_REQUIRES_CREWAI_MSG) + raise SystemExit(1) from None + + +def _require_get_user_id() -> Any: + try: + from crewai.events.listeners.tracing.utils import get_user_id + + return get_user_id + except ImportError: + console.print(_REQUIRES_CREWAI_MSG) + raise SystemExit(1) from None + + class ToolCommand(BaseCommand, PlusAPIMixin): """ A class to handle tool repository related operations for CrewAI projects. @@ -99,7 +120,8 @@ class ToolCommand(BaseCommand, PlusAPIMixin): encoded_tarball = None console.print("[bold blue]Discovering tools from your project...[/bold blue]") - available_exports = extract_available_exports() + project_utils = _require_project_utils() + available_exports = project_utils.extract_available_exports() if available_exports: console.print( @@ -108,7 +130,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin): console.print("[bold blue]Extracting tool metadata...[/bold blue]") try: - tools_metadata = extract_tools_metadata() + tools_metadata = project_utils.extract_tools_metadata() except Exception as e: console.print( f"[yellow]Warning: Could not extract tool metadata: {e}[/yellow]\n" @@ -202,6 +224,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin): console.print(f"Successfully installed {handle}", style="bold green") def login(self) -> None: + get_user_id = _require_get_user_id() login_response = self.plus_api_client.login_to_tool_repository( user_identifier=get_user_id() ) diff --git a/lib/cli/src/crewai_cli/user_data.py b/lib/cli/src/crewai_cli/user_data.py index 63f82ae6e..ee95797e8 100644 --- a/lib/cli/src/crewai_cli/user_data.py +++ b/lib/cli/src/crewai_cli/user_data.py @@ -1,71 +1,22 @@ -"""Standalone user-data helpers for the CLI package. - -These mirror the functions in ``crewai.events.listeners.tracing.utils`` but -depend only on the standard library + *appdirs* so that crewai-cli can work -without importing the full crewai framework. -""" +"""User-data helpers — re-exported from ``crewai_core.user_data``.""" from __future__ import annotations -import json -import logging -import os -from pathlib import Path -from typing import Any, cast - -import appdirs +from crewai_core.paths import db_storage_path as _db_storage_path +from crewai_core.user_data import ( + _load_user_data as _load_user_data, + _save_user_data as _save_user_data, + has_user_declined_tracing as has_user_declined_tracing, + is_tracing_enabled as is_tracing_enabled, + update_user_data as update_user_data, +) -logger = logging.getLogger(__name__) - - -def _get_project_directory_name() -> str: - return os.environ.get("CREWAI_STORAGE_DIR", Path.cwd().name) - - -def _db_storage_path() -> str: - app_name = _get_project_directory_name() - app_author = "CrewAI" - data_dir = Path(appdirs.user_data_dir(app_name, app_author)) - data_dir.mkdir(parents=True, exist_ok=True) - return str(data_dir) - - -def _user_data_file() -> Path: - base = Path(_db_storage_path()) - base.mkdir(parents=True, exist_ok=True) - return base / ".crewai_user.json" - - -def _load_user_data() -> dict[str, Any]: - p = _user_data_file() - if p.exists(): - try: - return cast(dict[str, Any], json.loads(p.read_text())) - except (json.JSONDecodeError, OSError, PermissionError) as e: - logger.warning("Failed to load user data: %s", e) - return {} - - -def _save_user_data(data: dict[str, Any]) -> None: - try: - p = _user_data_file() - p.write_text(json.dumps(data, indent=2)) - except (OSError, PermissionError) as e: - logger.warning("Failed to save user data: %s", e) - - -def is_tracing_enabled() -> bool: - """Check if tracing is enabled. - - Mirrors ``crewai.events.listeners.tracing.utils.is_tracing_enabled``: - consent only *blocks* tracing; activation requires - ``CREWAI_TRACING_ENABLED=true``. - """ - data = _load_user_data() - if ( - data.get("first_execution_done", False) - and data.get("trace_consent", False) is False - ): - return False - return os.getenv("CREWAI_TRACING_ENABLED", "false").lower() == "true" +__all__ = [ + "_db_storage_path", + "_load_user_data", + "_save_user_data", + "has_user_declined_tracing", + "is_tracing_enabled", + "update_user_data", +] diff --git a/lib/cli/src/crewai_cli/version.py b/lib/cli/src/crewai_cli/version.py index bae5d0eef..d763d636a 100644 --- a/lib/cli/src/crewai_cli/version.py +++ b/lib/cli/src/crewai_cli/version.py @@ -10,7 +10,7 @@ from urllib import request from urllib.error import URLError import appdirs -from crewai.utilities.version import get_crewai_version as get_crewai_version +from crewai_core.version import get_crewai_version as get_crewai_version from packaging.version import InvalidVersion, Version, parse diff --git a/lib/cli/tests/tools/test_main.py b/lib/cli/tests/tools/test_main.py index 94e0f2e32..b232dc5f8 100644 --- a/lib/cli/tests/tools/test_main.py +++ b/lib/cli/tests/tools/test_main.py @@ -184,11 +184,11 @@ def test_publish_when_not_in_sync(mock_is_synced, mock_fetch, capsys, tool_comma @patch("crewai_cli.plus_api.PlusAPI.publish_tool") @patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=False) @patch( - "crewai_cli.tools.main.extract_available_exports", + "crewai.utilities.project_utils.extract_available_exports", return_value=[{"name": "SampleTool"}], ) @patch( - "crewai_cli.tools.main.extract_tools_metadata", + "crewai.utilities.project_utils.extract_tools_metadata", return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}], ) @patch("crewai_cli.tools.main.ToolCommand._print_current_organization") @@ -250,11 +250,11 @@ def test_publish_when_not_in_sync_and_force( @patch("crewai_cli.plus_api.PlusAPI.publish_tool") @patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=True) @patch( - "crewai_cli.tools.main.extract_available_exports", + "crewai.utilities.project_utils.extract_available_exports", return_value=[{"name": "SampleTool"}], ) @patch( - "crewai_cli.tools.main.extract_tools_metadata", + "crewai.utilities.project_utils.extract_tools_metadata", return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}], ) def test_publish_success( @@ -311,11 +311,11 @@ def test_publish_success( ) @patch("crewai_cli.plus_api.PlusAPI.publish_tool") @patch( - "crewai_cli.tools.main.extract_available_exports", + "crewai.utilities.project_utils.extract_available_exports", return_value=[{"name": "SampleTool"}], ) @patch( - "crewai_cli.tools.main.extract_tools_metadata", + "crewai.utilities.project_utils.extract_tools_metadata", return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}], ) def test_publish_failure( @@ -357,11 +357,11 @@ def test_publish_failure( ) @patch("crewai_cli.plus_api.PlusAPI.publish_tool") @patch( - "crewai_cli.tools.main.extract_available_exports", + "crewai.utilities.project_utils.extract_available_exports", return_value=[{"name": "SampleTool"}], ) @patch( - "crewai_cli.tools.main.extract_tools_metadata", + "crewai.utilities.project_utils.extract_tools_metadata", return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}], ) def test_publish_api_error( @@ -404,11 +404,11 @@ def test_publish_api_error( @patch("crewai_cli.plus_api.PlusAPI.publish_tool") @patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=True) @patch( - "crewai_cli.tools.main.extract_available_exports", + "crewai.utilities.project_utils.extract_available_exports", return_value=[{"name": "SampleTool"}], ) @patch( - "crewai_cli.tools.main.extract_tools_metadata", + "crewai.utilities.project_utils.extract_tools_metadata", side_effect=Exception("Failed to extract metadata"), ) def test_publish_metadata_extraction_failure_continues_with_warning( diff --git a/lib/crewai-core/README.md b/lib/crewai-core/README.md new file mode 100644 index 000000000..3cf166d7a --- /dev/null +++ b/lib/crewai-core/README.md @@ -0,0 +1,8 @@ +# crewai-core + +Shared utilities used by both `crewai` and `crewai-cli`: version lookup, storage +paths, user-data helpers, telemetry, and the printer. + +This package is a leaf — it has no dependency on the `crewai` framework — and is +pulled in transitively by `crewai` and `crewai-cli`. End users do not normally +install it directly. diff --git a/lib/crewai-core/pyproject.toml b/lib/crewai-core/pyproject.toml new file mode 100644 index 000000000..b38072479 --- /dev/null +++ b/lib/crewai-core/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "crewai-core" +dynamic = ["version"] +description = "Shared utilities for CrewAI — version, paths, user-data, telemetry, printer." +readme = "README.md" +authors = [ + { name = "Greyson R. LaLonde", email = "greyson@crewai.com" } +] +requires-python = ">=3.10, <3.14" +dependencies = [ + "appdirs~=1.4.4", + "portalocker~=2.7.0", + "rich>=13.7.1", + "opentelemetry-api~=1.34.0", + "opentelemetry-sdk~=1.34.0", + "opentelemetry-exporter-otlp-proto-http~=1.34.0", +] + +[project.urls] +Homepage = "https://crewai.com" +Documentation = "https://docs.crewai.com" +Repository = "https://github.com/crewAIInc/crewAI" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.version] +path = "src/crewai_core/__init__.py" + +[tool.hatch.build.targets.wheel] +packages = ["src/crewai_core"] diff --git a/lib/crewai-core/src/crewai_core/__init__.py b/lib/crewai-core/src/crewai_core/__init__.py new file mode 100644 index 000000000..576c3fdf8 --- /dev/null +++ b/lib/crewai-core/src/crewai_core/__init__.py @@ -0,0 +1 @@ +__version__ = "1.14.5a2" diff --git a/lib/crewai-core/src/crewai_core/constants.py b/lib/crewai-core/src/crewai_core/constants.py new file mode 100644 index 000000000..06aeb69e0 --- /dev/null +++ b/lib/crewai-core/src/crewai_core/constants.py @@ -0,0 +1,12 @@ +"""Constants shared by both crewai and crewai-cli.""" + +from __future__ import annotations + +from typing import Final + + +CREWAI_TRAINED_AGENTS_FILE_ENV: Final[str] = "CREWAI_TRAINED_AGENTS_FILE" +TRAINING_DATA_FILE: Final[str] = "training_data.pkl" +TRAINED_AGENTS_DATA_FILE: Final[str] = "trained_agents_data.pkl" +KNOWLEDGE_DIRECTORY: Final[str] = "knowledge" +MAX_FILE_NAME_LENGTH: Final[int] = 255 diff --git a/lib/crewai-core/src/crewai_core/lock_store.py b/lib/crewai-core/src/crewai_core/lock_store.py new file mode 100644 index 000000000..16705d3ae --- /dev/null +++ b/lib/crewai-core/src/crewai_core/lock_store.py @@ -0,0 +1,89 @@ +"""Centralised lock factory. + +If ``REDIS_URL`` is set and the ``redis`` package is installed, locks are +distributed via ``portalocker.RedisLock``. Otherwise, falls back to the +standard file-based ``portalocker.Lock`` in the system temp dir. +""" + +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import contextmanager +from functools import lru_cache +from hashlib import md5 +import logging +import os +import tempfile +from typing import TYPE_CHECKING, Final + +import portalocker +import portalocker.exceptions + + +if TYPE_CHECKING: + import redis + + +logger = logging.getLogger(__name__) + +_REDIS_URL: str | None = os.environ.get("REDIS_URL") + +_DEFAULT_TIMEOUT: Final[int] = 120 + + +def _redis_available() -> bool: + """Return True if redis is installed and REDIS_URL is set.""" + if not _REDIS_URL: + return False + try: + import redis # noqa: F401 + + return True + except ImportError: + return False + + +@lru_cache(maxsize=1) +def _redis_connection() -> redis.Redis: + """Return a cached Redis connection, creating one on first call.""" + from redis import Redis + + if _REDIS_URL is None: + raise ValueError("REDIS_URL environment variable is not set") + return Redis.from_url(_REDIS_URL) + + +@contextmanager +def lock(name: str, *, timeout: float = _DEFAULT_TIMEOUT) -> Iterator[None]: + """Acquire a named lock, yielding while it is held. + + Args: + name: A human-readable lock name (e.g. ``"chromadb_init"``). + Automatically namespaced to avoid collisions. + timeout: Maximum seconds to wait for the lock before raising. + """ + channel = f"crewai:{md5(name.encode(), usedforsecurity=False).hexdigest()}" + + if _redis_available(): + with portalocker.RedisLock( + channel=channel, + connection=_redis_connection(), + timeout=timeout, + ): + yield + else: + lock_dir = tempfile.gettempdir() + lock_path = os.path.join(lock_dir, f"{channel}.lock") + try: + pl = portalocker.Lock(lock_path, timeout=timeout) + pl.acquire() + except portalocker.exceptions.BaseLockException as exc: + raise portalocker.exceptions.LockException( + f"Failed to acquire lock '{name}' at {lock_path} " + f"(timeout={timeout}s). This commonly occurs in " + f"multi-process environments. " + ) from exc + try: + yield + finally: + pl.release() # type: ignore[no-untyped-call] diff --git a/lib/crewai-core/src/crewai_core/paths.py b/lib/crewai-core/src/crewai_core/paths.py new file mode 100644 index 000000000..611265459 --- /dev/null +++ b/lib/crewai-core/src/crewai_core/paths.py @@ -0,0 +1,26 @@ +"""Path management utilities for CrewAI storage and configuration.""" + +from __future__ import annotations + +import os +from pathlib import Path + +import appdirs + + +def get_project_directory_name() -> str: + """Return the current project directory name (or ``CREWAI_STORAGE_DIR``).""" + return os.environ.get("CREWAI_STORAGE_DIR", Path.cwd().name) + + +def db_storage_path() -> str: + """Return the path for SQLite database / app-data storage. + + Creates the directory if it does not exist. + """ + app_name = get_project_directory_name() + app_author = "CrewAI" + + data_dir = Path(appdirs.user_data_dir(app_name, app_author)) + data_dir.mkdir(parents=True, exist_ok=True) + return str(data_dir) diff --git a/lib/crewai-core/src/crewai_core/printer.py b/lib/crewai-core/src/crewai_core/printer.py new file mode 100644 index 000000000..9f12a2ff6 --- /dev/null +++ b/lib/crewai-core/src/crewai_core/printer.py @@ -0,0 +1,103 @@ +"""Colored console-output utilities and the shared output-suppression flag.""" + +from __future__ import annotations + +from contextvars import ContextVar +from typing import TYPE_CHECKING, Final, Literal, NamedTuple + + +if TYPE_CHECKING: + from _typeshed import SupportsWrite + + +_suppress_console_output: ContextVar[bool] = ContextVar( + "_suppress_console_output", default=False +) + + +def set_suppress_console_output(suppress: bool) -> object: + """Toggle suppression of console output for the current context. + + Returns a token that can be passed to ``ContextVar.reset`` to restore the + previous value. + """ + return _suppress_console_output.set(suppress) + + +def should_suppress_console_output() -> bool: + """Return True if console output should currently be suppressed.""" + return _suppress_console_output.get() + + +PrinterColor = Literal[ + "purple", + "bold_purple", + "green", + "bold_green", + "cyan", + "bold_cyan", + "magenta", + "bold_magenta", + "yellow", + "bold_yellow", + "red", + "blue", + "bold_blue", +] + +_COLOR_CODES: Final[dict[PrinterColor, str]] = { + "purple": "\033[95m", + "bold_purple": "\033[1m\033[95m", + "red": "\033[91m", + "bold_green": "\033[1m\033[92m", + "green": "\033[32m", + "blue": "\033[94m", + "bold_blue": "\033[1m\033[94m", + "yellow": "\033[93m", + "bold_yellow": "\033[1m\033[93m", + "cyan": "\033[96m", + "bold_cyan": "\033[1m\033[96m", + "magenta": "\033[35m", + "bold_magenta": "\033[1m\033[35m", +} + +RESET: Final[str] = "\033[0m" + + +class ColoredText(NamedTuple): + """Text plus an optional color, used for multicolor lines.""" + + text: str + color: PrinterColor | None + + +class Printer: + """Handles colored console output formatting.""" + + @staticmethod + def print( + content: str | list[ColoredText], + color: PrinterColor | None = None, + sep: str | None = " ", + end: str | None = "\n", + file: SupportsWrite[str] | None = None, + flush: Literal[False] = False, + ) -> None: + """Print ``content`` with optional color, honoring suppression context.""" + if should_suppress_console_output(): + return + if isinstance(content, str): + content = [ColoredText(content, color)] + print( + "".join( + f"{_COLOR_CODES[c.color] if c.color else ''}{c.text}{RESET}" + for c in content + ), + sep=sep, + end=end, + file=file, + flush=flush, + ) + + +PRINTER: Printer = Printer() diff --git a/lib/crewai-core/src/crewai_core/py.typed b/lib/crewai-core/src/crewai_core/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/lib/crewai-core/src/crewai_core/telemetry.py b/lib/crewai-core/src/crewai_core/telemetry.py new file mode 100644 index 000000000..d69f5aeaa --- /dev/null +++ b/lib/crewai-core/src/crewai_core/telemetry.py @@ -0,0 +1,262 @@ +"""Anonymous telemetry collection — base implementation. + +This module is the leaf telemetry layer used by both ``crewai`` (which extends +it with framework-specific spans + event-bus signal hooks) and ``crewai-cli`` +(which uses it directly to emit deployment / template / flow-creation spans). + +No prompts, task descriptions, agent backstories/goals, responses, or sensitive +data are collected. +""" + +from __future__ import annotations + +import asyncio +import atexit +from collections.abc import Callable +import contextlib +import logging +import os +import threading +from typing import Any, Final + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + SpanExportResult, +) +from opentelemetry.trace import Span, Status, StatusCode +from typing_extensions import Self + + +logger = logging.getLogger(__name__) + + +CREWAI_TELEMETRY_BASE_URL: Final[str] = "https://telemetry.crewai.com:4319" +CREWAI_TELEMETRY_SERVICE_NAME: Final[str] = "crewAI-telemetry" + + +def close_span(span: Span) -> None: + """Set span status to OK and end it.""" + span.set_status(Status(StatusCode.OK)) + span.end() + + +@contextlib.contextmanager +def suppress_warnings() -> Any: + """Suppress noisy warnings during otel provider setup.""" + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + yield + + +class SafeOTLPSpanExporter(OTLPSpanExporter): + """OTLP exporter that swallows export failures so telemetry never crashes the app.""" + + def export(self, spans: Any) -> SpanExportResult: + try: + return super().export(spans) + except Exception as e: + logger.debug("Telemetry export failed: %s", e) + return SpanExportResult.FAILURE + + +class Telemetry: + """Base telemetry: OTLP setup + the spans needed by the CLI. + + crewai's runtime extends this with crew/agent/task/tool/flow execution spans + and event-bus signal handlers (see ``crewai.telemetry.telemetry``). + """ + + _instance = None + _lock = threading.Lock() + + def __new__(cls) -> Self: + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self) -> None: + if hasattr(self, "_initialized") and self._initialized: + return + + self.ready: bool = False + self.trace_set: bool = False + self._initialized: bool = True + + if self._is_telemetry_disabled(): + return + + try: + self.resource = Resource( + attributes={SERVICE_NAME: CREWAI_TELEMETRY_SERVICE_NAME}, + ) + with suppress_warnings(): + self.provider = TracerProvider(resource=self.resource) + + processor = BatchSpanProcessor( + SafeOTLPSpanExporter( + endpoint=f"{CREWAI_TELEMETRY_BASE_URL}/v1/traces", + timeout=30, + ) + ) + + self.provider.add_span_processor(processor) + self._register_shutdown_handlers() + self.ready = True + except Exception as e: + if isinstance( + e, + (SystemExit, KeyboardInterrupt, GeneratorExit, asyncio.CancelledError), + ): + raise + self.ready = False + + @classmethod + def _is_telemetry_disabled(cls) -> bool: + return ( + os.getenv("OTEL_SDK_DISABLED", "false").lower() == "true" + or os.getenv("CREWAI_DISABLE_TELEMETRY", "false").lower() == "true" + or os.getenv("CREWAI_DISABLE_TRACKING", "false").lower() == "true" + ) + + def _should_execute_telemetry(self) -> bool: + return self.ready and not self._is_telemetry_disabled() + + def _register_shutdown_handlers(self) -> None: + """Register an atexit flush. Subclasses may extend with signal hooks.""" + atexit.register(self._shutdown) + + def _shutdown(self) -> None: + if not self.ready: + return + try: + self.provider.force_flush(timeout_millis=5000) + self.provider.shutdown() + self.ready = False + except Exception as e: + logger.debug("Telemetry shutdown failed: %s", e) + + def set_tracer(self) -> None: + """Install our TracerProvider as the global one (idempotent).""" + if self.ready and not self.trace_set: + try: + with suppress_warnings(): + trace.set_tracer_provider(self.provider) + self.trace_set = True + except Exception as e: + logger.debug("Failed to set tracer provider: %s", e) + self.ready = False + self.trace_set = False + + def _safe_telemetry_operation( + self, operation: Callable[[], Span | None] + ) -> Span | None: + if not self._should_execute_telemetry(): + return None + try: + return operation() + except Exception as e: + logger.debug("Telemetry operation failed: %s", e) + return None + + def _add_attribute(self, span: Span | None, key: str, value: Any) -> None: + if span is None: + return + + def _operation() -> None: + return span.set_attribute(key, value) + + self._safe_telemetry_operation(_operation) + + # --- CLI-facing spans --------------------------------------------------- + + def deploy_signup_error_span(self) -> None: + """Records when an error occurs during the deployment signup process.""" + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Deploy Signup Error") + close_span(span) + + self._safe_telemetry_operation(_operation) + + def start_deployment_span(self, uuid: str | None = None) -> None: + """Records the start of a deployment process.""" + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Start Deployment") + if uuid: + self._add_attribute(span, "uuid", uuid) + close_span(span) + + self._safe_telemetry_operation(_operation) + + def create_crew_deployment_span(self) -> None: + """Records the creation of a new crew deployment.""" + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Create Crew Deployment") + close_span(span) + + self._safe_telemetry_operation(_operation) + + def get_crew_logs_span( + self, uuid: str | None, log_type: str = "deployment" + ) -> None: + """Records the retrieval of crew logs.""" + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Get Crew Logs") + self._add_attribute(span, "log_type", log_type) + if uuid: + self._add_attribute(span, "uuid", uuid) + close_span(span) + + self._safe_telemetry_operation(_operation) + + def remove_crew_span(self, uuid: str | None = None) -> None: + """Records the removal of a crew.""" + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Remove Crew") + if uuid: + self._add_attribute(span, "uuid", uuid) + close_span(span) + + self._safe_telemetry_operation(_operation) + + def flow_creation_span(self, flow_name: str) -> None: + """Records the creation of a new flow.""" + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Flow Creation") + self._add_attribute(span, "flow_name", flow_name) + close_span(span) + + self._safe_telemetry_operation(_operation) + + def template_installed_span(self, template_name: str) -> None: + """Records when a template is downloaded and installed.""" + from crewai_core.version import get_crewai_version + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Template Installed") + self._add_attribute(span, "crewai_version", get_crewai_version()) + self._add_attribute(span, "template_name", template_name) + close_span(span) + + self._safe_telemetry_operation(_operation) diff --git a/lib/crewai-core/src/crewai_core/user_data.py b/lib/crewai-core/src/crewai_core/user_data.py new file mode 100644 index 000000000..85ba6097c --- /dev/null +++ b/lib/crewai-core/src/crewai_core/user_data.py @@ -0,0 +1,85 @@ +"""Persistent per-user data + tracing-consent helpers. + +This is the single source of truth for the ``.crewai_user.json`` file used by +both crewai (to record trace consent) and crewai-cli (to read/write it via +``crewai traces enable/disable/status``). +""" + +from __future__ import annotations + +import json +import logging +import os +from pathlib import Path +from typing import Any, cast + +from crewai_core.lock_store import lock as store_lock +from crewai_core.paths import db_storage_path + + +logger = logging.getLogger(__name__) + + +def _user_data_file() -> Path: + base = Path(db_storage_path()) + base.mkdir(parents=True, exist_ok=True) + return base / ".crewai_user.json" + + +def _user_data_lock_name() -> str: + """Return a stable lock name for the user data file.""" + return f"file:{os.path.realpath(_user_data_file())}" + + +def _load_user_data() -> dict[str, Any]: + """Read the user-data JSON file, returning ``{}`` on missing/corrupt.""" + p = _user_data_file() + if p.exists(): + try: + return cast(dict[str, Any], json.loads(p.read_text())) + except (json.JSONDecodeError, OSError, PermissionError) as e: + logger.warning("Failed to load user data: %s", e) + return {} + + +def _save_user_data(data: dict[str, Any]) -> None: + """Write the full user-data dict, ignoring write errors with a warning.""" + try: + p = _user_data_file() + p.write_text(json.dumps(data, indent=2)) + except (OSError, PermissionError) as e: + logger.warning("Failed to save user data: %s", e) + + +def update_user_data(updates: dict[str, Any]) -> None: + """Atomically read-modify-write the user data file under a file lock. + + Args: + updates: Key-value pairs to merge into the existing user data. + """ + try: + with store_lock(_user_data_lock_name()): + data = _load_user_data() + data.update(updates) + _save_user_data(data) + except (OSError, PermissionError) as e: + logger.warning("Failed to update user data: %s", e) + + +def has_user_declined_tracing() -> bool: + """Return True if the user has explicitly declined trace collection.""" + data = _load_user_data() + if data.get("first_execution_done", False): + return data.get("trace_consent", False) is False + return False + + +def is_tracing_enabled() -> bool: + """Return True if tracing should currently be active. + + Consent only *blocks* tracing; activation requires + ``CREWAI_TRACING_ENABLED=true`` in the environment. + """ + if has_user_declined_tracing(): + return False + return os.getenv("CREWAI_TRACING_ENABLED", "false").lower() == "true" diff --git a/lib/crewai-core/src/crewai_core/version.py b/lib/crewai-core/src/crewai_core/version.py new file mode 100644 index 000000000..6cf87c866 --- /dev/null +++ b/lib/crewai-core/src/crewai_core/version.py @@ -0,0 +1,23 @@ +"""Version utilities for CrewAI.""" + +from __future__ import annotations + +from functools import cache +import importlib.metadata + + +@cache +def get_crewai_version() -> str: + """Return the installed crewAI version string. + + Falls back to ``"unknown"`` when neither crewai nor crewai-core are + pip-installed (e.g. running directly from a source checkout). + """ + try: + return importlib.metadata.version("crewai") + except importlib.metadata.PackageNotFoundError: + pass + try: + return importlib.metadata.version("crewai-core") + except importlib.metadata.PackageNotFoundError: + return "unknown" diff --git a/lib/crewai-core/tests/__init__.py b/lib/crewai-core/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/crewai-core/tests/test_smoke.py b/lib/crewai-core/tests/test_smoke.py new file mode 100644 index 000000000..d9f0692af --- /dev/null +++ b/lib/crewai-core/tests/test_smoke.py @@ -0,0 +1,92 @@ +"""Smoke tests for the crewai-core leaf modules.""" + +from __future__ import annotations + +import os +from pathlib import Path + +from crewai_core import ( + constants, + lock_store, + paths, + printer, + user_data, + version, +) +import pytest + + +def test_version_returns_string() -> None: + v = version.get_crewai_version() + assert isinstance(v, str) and v + + +def test_paths_creates_storage_dir( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("CREWAI_STORAGE_DIR", str(tmp_path / "store")) + monkeypatch.setattr( + "crewai_core.paths.appdirs.user_data_dir", + lambda app, author: str(tmp_path / app), + ) + out = paths.db_storage_path() + assert Path(out).exists() + + +def test_constants_exposes_env_keys() -> None: + assert constants.CREWAI_TRAINED_AGENTS_FILE_ENV == "CREWAI_TRAINED_AGENTS_FILE" + + +def test_printer_emits_when_not_suppressed(capsys: pytest.CaptureFixture[str]) -> None: + printer.PRINTER.print("hello", color="green") + out = capsys.readouterr().out + assert "hello" in out + + +def test_printer_respects_suppression(capsys: pytest.CaptureFixture[str]) -> None: + token = printer.set_suppress_console_output(True) + try: + printer.PRINTER.print("hidden") + finally: + printer._suppress_console_output.reset(token) # type: ignore[arg-type] + assert "hidden" not in capsys.readouterr().out + + +def test_lock_acquires_and_releases() -> None: + with lock_store.lock("crewai_core.tests.smoke", timeout=5): + pass + + +def test_user_data_round_trip(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("CREWAI_STORAGE_DIR", "crewai_core_test_user_data") + monkeypatch.setattr( + "crewai_core.paths.appdirs.user_data_dir", + lambda app, author: str(tmp_path / app), + ) + user_data.update_user_data({"trace_consent": True, "first_execution_done": True}) + data = user_data._load_user_data() + assert data == {"trace_consent": True, "first_execution_done": True} + assert user_data.has_user_declined_tracing() is False + monkeypatch.setenv("CREWAI_TRACING_ENABLED", "true") + assert user_data.is_tracing_enabled() is True + monkeypatch.delenv("CREWAI_TRACING_ENABLED", raising=False) + assert user_data.is_tracing_enabled() is False # consent without env var = off + + +def test_user_data_decline_blocks( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("CREWAI_STORAGE_DIR", "crewai_core_test_decline") + monkeypatch.setattr( + "crewai_core.paths.appdirs.user_data_dir", + lambda app, author: str(tmp_path / app), + ) + monkeypatch.setenv("CREWAI_TRACING_ENABLED", "true") + user_data.update_user_data({"trace_consent": False, "first_execution_done": True}) + assert user_data.has_user_declined_tracing() is True + assert user_data.is_tracing_enabled() is False + + +def test_unused_var_warning_silenced() -> None: + # Touch os to keep the import (used by env-var fixtures above) + assert os.environ is not None diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 6234bc217..fc087673f 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -8,6 +8,8 @@ authors = [ ] requires-python = ">=3.10, <3.14" dependencies = [ + # Shared utilities (version, paths, user_data, telemetry, printer) + "crewai-core", # Core Dependencies "pydantic>=2.11.9,<2.13", "openai>=2.30.0,<3", diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index f4d55fe80..1dac07606 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -15,6 +15,7 @@ import inspect import logging from typing import TYPE_CHECKING, Annotated, Any, Literal, cast +from crewai_core.printer import PRINTER from pydantic import ( AliasChoices, BaseModel, @@ -69,7 +70,6 @@ from crewai.utilities.agent_utils import ( from crewai.utilities.constants import TRAINING_DATA_FILE from crewai.utilities.file_store import aget_all_files, get_all_files from crewai.utilities.i18n import I18N_DEFAULT -from crewai.utilities.printer import PRINTER from crewai.utilities.string_utils import sanitize_tool_name from crewai.utilities.token_counter_callback import TokenCalcHandler from crewai.utilities.tool_utils import ( diff --git a/lib/crewai/src/crewai/agents/step_executor.py b/lib/crewai/src/crewai/agents/step_executor.py index df834e3e4..5fe517389 100644 --- a/lib/crewai/src/crewai/agents/step_executor.py +++ b/lib/crewai/src/crewai/agents/step_executor.py @@ -18,6 +18,7 @@ import json import time from typing import TYPE_CHECKING, Any, cast +from crewai_core.printer import PRINTER from pydantic import BaseModel from crewai.agents.parser import AgentAction, AgentFinish @@ -40,7 +41,6 @@ from crewai.utilities.agent_utils import ( ) from crewai.utilities.i18n import I18N_DEFAULT from crewai.utilities.planning_types import TodoItem -from crewai.utilities.printer import PRINTER from crewai.utilities.step_execution_context import StepExecutionContext, StepResult from crewai.utilities.string_utils import sanitize_tool_name from crewai.utilities.tool_utils import execute_tool_and_check_finality diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index f7321b6ae..60f163155 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -54,6 +54,8 @@ except ImportError: return [] +from crewai_core.printer import PrinterColor + from crewai.agent import Agent from crewai.agents.agent_builder.base_agent import ( BaseAgent, @@ -132,7 +134,6 @@ from crewai.utilities.i18n import get_i18n from crewai.utilities.llm_utils import create_llm from crewai.utilities.logger import Logger from crewai.utilities.planning_handler import CrewPlanner -from crewai.utilities.printer import PrinterColor from crewai.utilities.rpm_controller import RPMController from crewai.utilities.streaming import ( create_async_chunk_generator, diff --git a/lib/crewai/src/crewai/events/listeners/tracing/utils.py b/lib/crewai/src/crewai/events/listeners/tracing/utils.py index 314922870..5db1570d6 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/utils.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/utils.py @@ -15,12 +15,20 @@ from typing import Any, cast import uuid import click +from crewai_core.lock_store import lock as store_lock +from crewai_core.user_data import ( + _load_user_data as _load_user_data, + _save_user_data as _save_user_data, + _user_data_file as _user_data_file, + _user_data_lock_name as _user_data_lock_name, + has_user_declined_tracing as has_user_declined_tracing, + is_tracing_enabled as is_tracing_enabled, + update_user_data as update_user_data, +) from rich.console import Console from rich.panel import Panel from rich.text import Text -from crewai.utilities.lock_store import lock as store_lock -from crewai.utilities.paths import db_storage_path from crewai.utilities.serialization import to_serializable @@ -123,69 +131,6 @@ def is_tracing_enabled_in_context() -> bool: return enabled if enabled is not None else False -def _user_data_file() -> Path: - base = Path(db_storage_path()) - base.mkdir(parents=True, exist_ok=True) - return base / ".crewai_user.json" - - -def _load_user_data() -> dict[str, Any]: - p = _user_data_file() - if p.exists(): - try: - return cast(dict[str, Any], json.loads(p.read_text())) - except (json.JSONDecodeError, OSError, PermissionError) as e: - logger.warning(f"Failed to load user data: {e}") - return {} - - -def _user_data_lock_name() -> str: - """Return a stable lock name for the user data file.""" - return f"file:{os.path.realpath(_user_data_file())}" - - -def update_user_data(updates: dict[str, Any]) -> None: - """Atomically read-modify-write the user data file. - - Args: - updates: Key-value pairs to merge into the existing user data. - """ - try: - with store_lock(_user_data_lock_name()): - data = _load_user_data() - data.update(updates) - p = _user_data_file() - p.write_text(json.dumps(data, indent=2)) - except (OSError, PermissionError) as e: - logger.warning(f"Failed to update user data: {e}") - - -def has_user_declined_tracing() -> bool: - """Check if user has explicitly declined trace collection. - - Returns: - True if user previously declined tracing, False otherwise. - """ - data = _load_user_data() - if data.get("first_execution_done", False): - return data.get("trace_consent", False) is False - return False - - -def is_tracing_enabled() -> bool: - """Check if tracing should be enabled. - - - Returns: - True if tracing is enabled and not disabled, False otherwise. - """ - # If user has explicitly declined tracing, never enable it - if has_user_declined_tracing(): - return False - - return os.getenv("CREWAI_TRACING_ENABLED", "false").lower() == "true" - - def on_first_execution_tracing_confirmation() -> bool: if _is_test_environment(): return False diff --git a/lib/crewai/src/crewai/events/utils/console_formatter.py b/lib/crewai/src/crewai/events/utils/console_formatter.py index 20a9692f9..83e224d0c 100644 --- a/lib/crewai/src/crewai/events/utils/console_formatter.py +++ b/lib/crewai/src/crewai/events/utils/console_formatter.py @@ -3,6 +3,10 @@ import os import threading from typing import Any, ClassVar, cast +from crewai_core.printer import ( + set_suppress_console_output as set_suppress_console_output, + should_suppress_console_output as should_suppress_console_output, +) from rich.console import Console from rich.live import Live from rich.panel import Panel @@ -15,31 +19,6 @@ _disable_version_check: ContextVar[bool] = ContextVar( "_disable_version_check", default=False ) -_suppress_console_output: ContextVar[bool] = ContextVar( - "_suppress_console_output", default=False -) - - -def set_suppress_console_output(suppress: bool) -> object: - """Set whether to suppress all console output. - - Args: - suppress: True to suppress output, False to show it. - - Returns: - A token that can be used to restore the previous value. - """ - return _suppress_console_output.set(suppress) - - -def should_suppress_console_output() -> bool: - """Check if console output should be suppressed. - - Returns: - True if output should be suppressed, False otherwise. - """ - return _suppress_console_output.get() - class ConsoleFormatter: tool_usage_counts: ClassVar[dict[str, int]] = {} diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index de6f2ad62..a650d917c 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -12,6 +12,7 @@ import threading from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast from uuid import uuid4 +from crewai_core.printer import PRINTER from pydantic import ( BaseModel, Field, @@ -99,7 +100,6 @@ from crewai.utilities.planning_types import ( TodoItem, TodoList, ) -from crewai.utilities.printer import PRINTER from crewai.utilities.step_execution_context import StepExecutionContext, StepResult from crewai.utilities.string_utils import sanitize_tool_name from crewai.utilities.tool_utils import execute_tool_and_check_finality diff --git a/lib/crewai/src/crewai/flow/persistence/decorators.py b/lib/crewai/src/crewai/flow/persistence/decorators.py index 937b557f4..f7881fdc3 100644 --- a/lib/crewai/src/crewai/flow/persistence/decorators.py +++ b/lib/crewai/src/crewai/flow/persistence/decorators.py @@ -30,11 +30,11 @@ import functools import logging from typing import TYPE_CHECKING, Any, Final, TypeVar, cast +from crewai_core.printer import PRINTER from pydantic import BaseModel from crewai.flow.persistence.base import FlowPersistence from crewai.flow.persistence.sqlite import SQLiteFlowPersistence -from crewai.utilities.printer import PRINTER if TYPE_CHECKING: diff --git a/lib/crewai/src/crewai/flow/persistence/sqlite.py b/lib/crewai/src/crewai/flow/persistence/sqlite.py index fa2e4e127..77289ab2f 100644 --- a/lib/crewai/src/crewai/flow/persistence/sqlite.py +++ b/lib/crewai/src/crewai/flow/persistence/sqlite.py @@ -9,12 +9,12 @@ from pathlib import Path import sqlite3 from typing import TYPE_CHECKING, Any +from crewai_core.lock_store import lock as store_lock +from crewai_core.paths import db_storage_path from pydantic import BaseModel, Field, PrivateAttr, model_validator from typing_extensions import Self from crewai.flow.persistence.base import FlowPersistence -from crewai.utilities.lock_store import lock as store_lock -from crewai.utilities.paths import db_storage_path if TYPE_CHECKING: diff --git a/lib/crewai/src/crewai/flow/utils.py b/lib/crewai/src/crewai/flow/utils.py index 652a38f4c..917ed40b9 100644 --- a/lib/crewai/src/crewai/flow/utils.py +++ b/lib/crewai/src/crewai/flow/utils.py @@ -22,6 +22,7 @@ import inspect import textwrap from typing import TYPE_CHECKING, Any +from crewai_core.printer import PRINTER from typing_extensions import TypeIs from crewai.flow.constants import AND_CONDITION, OR_CONDITION @@ -32,7 +33,6 @@ from crewai.flow.flow_wrappers import ( SimpleFlowCondition, ) from crewai.flow.types import FlowMethodCallable, FlowMethodName -from crewai.utilities.printer import PRINTER if TYPE_CHECKING: diff --git a/lib/crewai/src/crewai/hooks/llm_hooks.py b/lib/crewai/src/crewai/hooks/llm_hooks.py index bc3d1d17d..f64605c8e 100644 --- a/lib/crewai/src/crewai/hooks/llm_hooks.py +++ b/lib/crewai/src/crewai/hooks/llm_hooks.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, cast +from crewai_core.printer import PRINTER + from crewai.events.event_listener import event_listener from crewai.hooks.types import ( AfterLLMCallHookCallable, @@ -9,7 +11,6 @@ from crewai.hooks.types import ( BeforeLLMCallHookCallable, BeforeLLMCallHookType, ) -from crewai.utilities.printer import PRINTER if TYPE_CHECKING: diff --git a/lib/crewai/src/crewai/hooks/tool_hooks.py b/lib/crewai/src/crewai/hooks/tool_hooks.py index 6d9c015b5..70edf03fb 100644 --- a/lib/crewai/src/crewai/hooks/tool_hooks.py +++ b/lib/crewai/src/crewai/hooks/tool_hooks.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any +from crewai_core.printer import PRINTER + from crewai.events.event_listener import event_listener from crewai.hooks.types import ( AfterToolCallHookCallable, @@ -9,7 +11,6 @@ from crewai.hooks.types import ( BeforeToolCallHookCallable, BeforeToolCallHookType, ) -from crewai.utilities.printer import PRINTER if TYPE_CHECKING: diff --git a/lib/crewai/src/crewai/lite_agent.py b/lib/crewai/src/crewai/lite_agent.py index fbc9cf0b5..cd9823e15 100644 --- a/lib/crewai/src/crewai/lite_agent.py +++ b/lib/crewai/src/crewai/lite_agent.py @@ -35,6 +35,8 @@ if TYPE_CHECKING: from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig +from crewai_core.printer import PRINTER + from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess from crewai.agents.cache.cache_handler import CacheHandler @@ -92,7 +94,6 @@ from crewai.utilities.guardrail import process_guardrail, serialize_guardrail_fo from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType from crewai.utilities.i18n import I18N_DEFAULT from crewai.utilities.llm_utils import create_llm -from crewai.utilities.printer import PRINTER from crewai.utilities.pydantic_schema_utils import generate_model_description from crewai.utilities.token_counter_callback import TokenCalcHandler from crewai.utilities.tool_utils import execute_tool_and_check_finality diff --git a/lib/crewai/src/crewai/llms/base_llm.py b/lib/crewai/src/crewai/llms/base_llm.py index a9aa7dc01..86a3ba276 100644 --- a/lib/crewai/src/crewai/llms/base_llm.py +++ b/lib/crewai/src/crewai/llms/base_llm.py @@ -900,11 +900,12 @@ class BaseLLM(BaseModel, ABC): if from_agent is not None: return True + from crewai_core.printer import PRINTER + from crewai.hooks.llm_hooks import ( LLMCallHookContext, get_before_llm_call_hooks, ) - from crewai.utilities.printer import PRINTER before_hooks = get_before_llm_call_hooks() if not before_hooks: @@ -969,11 +970,12 @@ class BaseLLM(BaseModel, ABC): if from_agent is not None or not isinstance(response, str): return response + from crewai_core.printer import PRINTER + from crewai.hooks.llm_hooks import ( LLMCallHookContext, get_after_llm_call_hooks, ) - from crewai.utilities.printer import PRINTER after_hooks = get_after_llm_call_hooks() if not after_hooks: diff --git a/lib/crewai/src/crewai/memory/storage/kickoff_task_outputs_storage.py b/lib/crewai/src/crewai/memory/storage/kickoff_task_outputs_storage.py index 3f5f38c9f..2a9ab2e29 100644 --- a/lib/crewai/src/crewai/memory/storage/kickoff_task_outputs_storage.py +++ b/lib/crewai/src/crewai/memory/storage/kickoff_task_outputs_storage.py @@ -5,11 +5,12 @@ from pathlib import Path import sqlite3 from typing import Any +from crewai_core.lock_store import lock as store_lock +from crewai_core.paths import db_storage_path + from crewai.task import Task from crewai.utilities.crew_json_encoder import CrewJSONEncoder from crewai.utilities.errors import DatabaseError, DatabaseOperationError -from crewai.utilities.lock_store import lock as store_lock -from crewai.utilities.paths import db_storage_path logger = logging.getLogger(__name__) diff --git a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py index a7a2d3956..25793468b 100644 --- a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py +++ b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py @@ -12,10 +12,10 @@ import threading import time from typing import Any +from crewai_core.lock_store import lock as store_lock import lancedb # type: ignore[import-untyped] from crewai.memory.types import MemoryRecord, ScopeInfo -from crewai.utilities.lock_store import lock as store_lock _logger = logging.getLogger(__name__) @@ -68,7 +68,7 @@ class LanceDBStorage: if storage_dir: path = Path(storage_dir) / "memory" else: - from crewai.utilities.paths import db_storage_path + from crewai_core.paths import db_storage_path path = Path(db_storage_path()) / "memory" self._path = Path(path) diff --git a/lib/crewai/src/crewai/memory/storage/qdrant_edge_storage.py b/lib/crewai/src/crewai/memory/storage/qdrant_edge_storage.py index f20faa408..d819094e9 100644 --- a/lib/crewai/src/crewai/memory/storage/qdrant_edge_storage.py +++ b/lib/crewai/src/crewai/memory/storage/qdrant_edge_storage.py @@ -104,7 +104,7 @@ class QdrantEdgeStorage: if storage_dir: path = Path(storage_dir) / "memory" / "qdrant-edge" else: - from crewai.utilities.paths import db_storage_path + from crewai_core.paths import db_storage_path path = Path(db_storage_path()) / "memory" / "qdrant-edge" diff --git a/lib/crewai/src/crewai/rag/chromadb/client.py b/lib/crewai/src/crewai/rag/chromadb/client.py index 02f28c7f6..be52a4e17 100644 --- a/lib/crewai/src/crewai/rag/chromadb/client.py +++ b/lib/crewai/src/crewai/rag/chromadb/client.py @@ -10,6 +10,7 @@ from chromadb.api.types import ( EmbeddingFunction as ChromaEmbeddingFunction, QueryResult, ) +from crewai_core.lock_store import lock as store_lock from typing_extensions import Unpack from crewai.rag.chromadb.types import ( @@ -32,7 +33,6 @@ from crewai.rag.core.base_client import ( BaseCollectionParams, ) from crewai.rag.types import SearchResult -from crewai.utilities.lock_store import lock as store_lock from crewai.utilities.logger_utils import suppress_logging diff --git a/lib/crewai/src/crewai/rag/chromadb/constants.py b/lib/crewai/src/crewai/rag/chromadb/constants.py index 73b659fdf..bdeb7ed3a 100644 --- a/lib/crewai/src/crewai/rag/chromadb/constants.py +++ b/lib/crewai/src/crewai/rag/chromadb/constants.py @@ -3,7 +3,7 @@ import re from typing import Final -from crewai.utilities.paths import db_storage_path +from crewai_core.paths import db_storage_path DEFAULT_TENANT: Final[str] = "default_tenant" diff --git a/lib/crewai/src/crewai/rag/chromadb/factory.py b/lib/crewai/src/crewai/rag/chromadb/factory.py index f48425ab3..5e95bf9e8 100644 --- a/lib/crewai/src/crewai/rag/chromadb/factory.py +++ b/lib/crewai/src/crewai/rag/chromadb/factory.py @@ -3,10 +3,10 @@ import os from chromadb import PersistentClient +from crewai_core.lock_store import lock from crewai.rag.chromadb.client import ChromaDBClient from crewai.rag.chromadb.config import ChromaDBConfig -from crewai.utilities.lock_store import lock def create_client(config: ChromaDBConfig) -> ChromaDBClient: diff --git a/lib/crewai/src/crewai/rag/embeddings/providers/ibm/embedding_callable.py b/lib/crewai/src/crewai/rag/embeddings/providers/ibm/embedding_callable.py index 44e97149a..237ff4a5c 100644 --- a/lib/crewai/src/crewai/rag/embeddings/providers/ibm/embedding_callable.py +++ b/lib/crewai/src/crewai/rag/embeddings/providers/ibm/embedding_callable.py @@ -3,10 +3,10 @@ from typing import Any, cast from chromadb.api.types import Documents, EmbeddingFunction, Embeddings +from crewai_core.printer import PRINTER from typing_extensions import Unpack from crewai.rag.embeddings.providers.ibm.types import WatsonXProviderConfig -from crewai.utilities.printer import PRINTER class WatsonXEmbeddingFunction(EmbeddingFunction[Documents]): diff --git a/lib/crewai/src/crewai/rag/qdrant/constants.py b/lib/crewai/src/crewai/rag/qdrant/constants.py index 75e8e7c25..750cbc139 100644 --- a/lib/crewai/src/crewai/rag/qdrant/constants.py +++ b/lib/crewai/src/crewai/rag/qdrant/constants.py @@ -3,10 +3,9 @@ import os from typing import Final +from crewai_core.paths import db_storage_path from qdrant_client.models import Distance, VectorParams -from crewai.utilities.paths import db_storage_path - DEFAULT_VECTOR_PARAMS: Final = VectorParams(size=384, distance=Distance.COSINE) DEFAULT_EMBEDDING_MODEL: Final[str] = "sentence-transformers/all-MiniLM-L6-v2" diff --git a/lib/crewai/src/crewai/state/runtime.py b/lib/crewai/src/crewai/state/runtime.py index 471107997..2662266d2 100644 --- a/lib/crewai/src/crewai/state/runtime.py +++ b/lib/crewai/src/crewai/state/runtime.py @@ -14,6 +14,7 @@ import time from typing import TYPE_CHECKING, Any import uuid +from crewai_core.version import get_crewai_version from packaging.version import Version from pydantic import ( ModelWrapValidatorHandler, @@ -39,7 +40,6 @@ from crewai.state.checkpoint_config import CheckpointConfig from crewai.state.event_record import EventRecord from crewai.state.provider.core import BaseProvider from crewai.state.provider.json_provider import JsonProvider -from crewai.utilities.version import get_crewai_version logger = logging.getLogger(__name__) diff --git a/lib/crewai/src/crewai/task.py b/lib/crewai/src/crewai/task.py index 28136097f..b8b726b77 100644 --- a/lib/crewai/src/crewai/task.py +++ b/lib/crewai/src/crewai/task.py @@ -77,6 +77,8 @@ except ImportError: return [] +from crewai_core.printer import PRINTER + from crewai.types.callback import SerializableCallable from crewai.utilities.guardrail import ( process_guardrail, @@ -89,7 +91,6 @@ from crewai.utilities.guardrail_types import ( GuardrailsType, ) from crewai.utilities.i18n import I18N_DEFAULT -from crewai.utilities.printer import PRINTER from crewai.utilities.string_utils import interpolate_only diff --git a/lib/crewai/src/crewai/tools/tool_usage.py b/lib/crewai/src/crewai/tools/tool_usage.py index 09b44be17..0a004059a 100644 --- a/lib/crewai/src/crewai/tools/tool_usage.py +++ b/lib/crewai/src/crewai/tools/tool_usage.py @@ -9,6 +9,7 @@ from textwrap import dedent import time from typing import TYPE_CHECKING, Any, Literal +from crewai_core.printer import PRINTER import json5 from json_repair import repair_json # type: ignore[import-untyped] @@ -29,7 +30,6 @@ from crewai.utilities.agent_utils import ( ) from crewai.utilities.converter import Converter from crewai.utilities.i18n import I18N_DEFAULT -from crewai.utilities.printer import PRINTER from crewai.utilities.string_utils import sanitize_tool_name diff --git a/lib/crewai/src/crewai/utilities/__init__.py b/lib/crewai/src/crewai/utilities/__init__.py index b2c02dce0..9910d6ba0 100644 --- a/lib/crewai/src/crewai/utilities/__init__.py +++ b/lib/crewai/src/crewai/utilities/__init__.py @@ -1,3 +1,5 @@ +from crewai_core.printer import Printer + from crewai.utilities.converter import Converter, ConverterError from crewai.utilities.exceptions.context_window_exceeding_exception import ( LLMContextLengthExceededError, @@ -6,7 +8,6 @@ from crewai.utilities.file_handler import FileHandler from crewai.utilities.i18n import I18N from crewai.utilities.internal_instructor import InternalInstructor from crewai.utilities.logger import Logger -from crewai.utilities.printer import Printer from crewai.utilities.prompts import Prompts from crewai.utilities.rpm_controller import RPMController diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py index bfea4b67c..4255449c0 100644 --- a/lib/crewai/src/crewai/utilities/agent_utils.py +++ b/lib/crewai/src/crewai/utilities/agent_utils.py @@ -12,6 +12,7 @@ import json import re from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict +from crewai_core.printer import PRINTER, ColoredText, Printer from pydantic import BaseModel from rich.console import Console @@ -33,7 +34,6 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import ( LLMContextLengthExceededError, ) from crewai.utilities.i18n import I18N_DEFAULT -from crewai.utilities.printer import PRINTER, ColoredText, Printer from crewai.utilities.pydantic_schema_utils import generate_model_description from crewai.utilities.string_utils import sanitize_tool_name from crewai.utilities.token_counter_callback import TokenCalcHandler diff --git a/lib/crewai/src/crewai/utilities/constants.py b/lib/crewai/src/crewai/utilities/constants.py index 1f80dcbe6..92d438dec 100644 --- a/lib/crewai/src/crewai/utilities/constants.py +++ b/lib/crewai/src/crewai/utilities/constants.py @@ -1,15 +1,16 @@ from typing import Annotated, Final +from crewai_core.constants import ( + CREWAI_TRAINED_AGENTS_FILE_ENV as CREWAI_TRAINED_AGENTS_FILE_ENV, + KNOWLEDGE_DIRECTORY as KNOWLEDGE_DIRECTORY, + MAX_FILE_NAME_LENGTH as MAX_FILE_NAME_LENGTH, + TRAINED_AGENTS_DATA_FILE as TRAINED_AGENTS_DATA_FILE, + TRAINING_DATA_FILE as TRAINING_DATA_FILE, +) +from crewai_core.printer import PrinterColor from pydantic_core import CoreSchema -from crewai.utilities.printer import PrinterColor - -TRAINING_DATA_FILE: Final[str] = "training_data.pkl" -TRAINED_AGENTS_DATA_FILE: Final[str] = "trained_agents_data.pkl" -CREWAI_TRAINED_AGENTS_FILE_ENV: Final[str] = "CREWAI_TRAINED_AGENTS_FILE" -KNOWLEDGE_DIRECTORY: Final[str] = "knowledge" -MAX_FILE_NAME_LENGTH: Final[int] = 255 EMITTER_COLOR: Final[PrinterColor] = "bold_blue" CC_ENV_VAR: Final[str] = "CLAUDECODE" CODEX_ENV_VARS: Final[tuple[str, ...]] = ( diff --git a/lib/crewai/src/crewai/utilities/converter.py b/lib/crewai/src/crewai/utilities/converter.py index e8a73f192..d31b76f48 100644 --- a/lib/crewai/src/crewai/utilities/converter.py +++ b/lib/crewai/src/crewai/utilities/converter.py @@ -5,13 +5,13 @@ import json import re from typing import TYPE_CHECKING, Any, Final, TypedDict +from crewai_core.printer import PRINTER from pydantic import BaseModel, ValidationError from typing_extensions import Unpack from crewai.agents.agent_builder.utilities.base_output_converter import OutputConverter from crewai.utilities.i18n import I18N_DEFAULT from crewai.utilities.internal_instructor import InternalInstructor -from crewai.utilities.printer import PRINTER from crewai.utilities.pydantic_schema_utils import generate_model_description diff --git a/lib/crewai/src/crewai/utilities/crew_chat.py b/lib/crewai/src/crewai/utilities/crew_chat.py index bde7a4d38..0ae40ea5b 100644 --- a/lib/crewai/src/crewai/utilities/crew_chat.py +++ b/lib/crewai/src/crewai/utilities/crew_chat.py @@ -11,6 +11,7 @@ import time from typing import Any, Final, Literal import click +from crewai_core.printer import PRINTER from packaging import version import tomli @@ -19,7 +20,6 @@ from crewai.llm import LLM from crewai.llms.base_llm import BaseLLM from crewai.types.crew_chat import ChatInputField, ChatInputs from crewai.utilities.llm_utils import create_llm -from crewai.utilities.printer import PRINTER from crewai.utilities.project_utils import read_toml from crewai.utilities.types import LLMMessage from crewai.version import get_crewai_version diff --git a/lib/crewai/src/crewai/utilities/file_handler.py b/lib/crewai/src/crewai/utilities/file_handler.py index c456d58df..437e267d8 100644 --- a/lib/crewai/src/crewai/utilities/file_handler.py +++ b/lib/crewai/src/crewai/utilities/file_handler.py @@ -4,10 +4,9 @@ import os import pickle from typing import Any, TypedDict +from crewai_core.lock_store import lock as store_lock from typing_extensions import Unpack -from crewai.utilities.lock_store import lock as store_lock - class LogEntry(TypedDict, total=False): """TypedDict for log entry kwargs with optional fields for flexibility.""" diff --git a/lib/crewai/src/crewai/utilities/lock_store.py b/lib/crewai/src/crewai/utilities/lock_store.py index 363448d8d..875b1996d 100644 --- a/lib/crewai/src/crewai/utilities/lock_store.py +++ b/lib/crewai/src/crewai/utilities/lock_store.py @@ -1,88 +1,14 @@ -"""Centralised lock factory. - -If ``REDIS_URL`` is set and the ``redis`` package is installed, locks are distributed via -``portalocker.RedisLock``. Otherwise, falls back to the standard ``portalocker.Lock``. -""" +"""Deprecated: use ``crewai_core.lock_store`` instead.""" from __future__ import annotations -from collections.abc import Iterator -from contextlib import contextmanager -from functools import lru_cache -from hashlib import md5 -import logging -import os -import tempfile -from typing import TYPE_CHECKING, Final +import warnings -import portalocker -import portalocker.exceptions +from crewai_core.lock_store import lock as lock -if TYPE_CHECKING: - import redis - - -logger = logging.getLogger(__name__) - -_REDIS_URL: str | None = os.environ.get("REDIS_URL") - -_DEFAULT_TIMEOUT: Final[int] = 120 - - -def _redis_available() -> bool: - """Return True if redis is installed and REDIS_URL is set.""" - if not _REDIS_URL: - return False - try: - import redis # noqa: F401 - - return True - except ImportError: - return False - - -@lru_cache(maxsize=1) -def _redis_connection() -> redis.Redis: - """Return a cached Redis connection, creating one on first call.""" - from redis import Redis - - if _REDIS_URL is None: - raise ValueError("REDIS_URL environment variable is not set") - return Redis.from_url(_REDIS_URL) - - -@contextmanager -def lock(name: str, *, timeout: float = _DEFAULT_TIMEOUT) -> Iterator[None]: - """Acquire a named lock, yielding while it is held. - - Args: - name: A human-readable lock name (e.g. ``"chromadb_init"``). - Automatically namespaced to avoid collisions. - timeout: Maximum seconds to wait for the lock before raising. - """ - channel = f"crewai:{md5(name.encode(), usedforsecurity=False).hexdigest()}" - - if _redis_available(): - with portalocker.RedisLock( - channel=channel, - connection=_redis_connection(), - timeout=timeout, - ): - yield - else: - lock_dir = tempfile.gettempdir() - lock_path = os.path.join(lock_dir, f"{channel}.lock") - try: - pl = portalocker.Lock(lock_path, timeout=timeout) - pl.acquire() - except portalocker.exceptions.BaseLockException as exc: - raise portalocker.exceptions.LockException( - f"Failed to acquire lock '{name}' at {lock_path} " - f"(timeout={timeout}s). This commonly occurs in " - f"multi-process environments. " - ) from exc - try: - yield - finally: - pl.release() # type: ignore[no-untyped-call] +warnings.warn( + "crewai.utilities.lock_store is deprecated; import from crewai_core.lock_store.", + DeprecationWarning, + stacklevel=2, +) diff --git a/lib/crewai/src/crewai/utilities/logger.py b/lib/crewai/src/crewai/utilities/logger.py index afc09d693..30c54a2e9 100644 --- a/lib/crewai/src/crewai/utilities/logger.py +++ b/lib/crewai/src/crewai/utilities/logger.py @@ -1,9 +1,8 @@ from datetime import datetime +from crewai_core.printer import PRINTER, ColoredText, PrinterColor from pydantic import BaseModel, Field -from crewai.utilities.printer import PRINTER, ColoredText, PrinterColor - class Logger(BaseModel): verbose: bool = Field( diff --git a/lib/crewai/src/crewai/utilities/paths.py b/lib/crewai/src/crewai/utilities/paths.py index 3612af9c7..59627fa3b 100644 --- a/lib/crewai/src/crewai/utilities/paths.py +++ b/lib/crewai/src/crewai/utilities/paths.py @@ -1,25 +1,17 @@ -"""Path management utilities for CrewAI storage and configuration.""" +"""Deprecated: use ``crewai_core.paths`` instead.""" -import os -from pathlib import Path +from __future__ import annotations -import appdirs +import warnings + +from crewai_core.paths import ( + db_storage_path as db_storage_path, + get_project_directory_name as get_project_directory_name, +) -def db_storage_path() -> str: - """Returns the path for SQLite database storage. - - Returns: - str: Full path to the SQLite database file - """ - app_name = get_project_directory_name() - app_author = "CrewAI" - - data_dir = Path(appdirs.user_data_dir(app_name, app_author)) - data_dir.mkdir(parents=True, exist_ok=True) - return str(data_dir) - - -def get_project_directory_name() -> str: - """Returns the current project directory name.""" - return os.environ.get("CREWAI_STORAGE_DIR", Path.cwd().name) +warnings.warn( + "crewai.utilities.paths is deprecated; import from crewai_core.paths.", + DeprecationWarning, + stacklevel=2, +) diff --git a/lib/crewai/src/crewai/utilities/printer.py b/lib/crewai/src/crewai/utilities/printer.py index bb0dfecba..56ceb14bb 100644 --- a/lib/crewai/src/crewai/utilities/printer.py +++ b/lib/crewai/src/crewai/utilities/printer.py @@ -1,98 +1,19 @@ -"""Utility for colored console output.""" +"""Deprecated: use ``crewai_core.printer`` instead.""" from __future__ import annotations -from typing import TYPE_CHECKING, Final, Literal, NamedTuple +import warnings -from crewai.events.utils.console_formatter import should_suppress_console_output +from crewai_core.printer import ( + PRINTER as PRINTER, + ColoredText as ColoredText, + Printer as Printer, + PrinterColor as PrinterColor, +) -if TYPE_CHECKING: - from _typeshed import SupportsWrite - -PrinterColor = Literal[ - "purple", - "bold_purple", - "green", - "bold_green", - "cyan", - "bold_cyan", - "magenta", - "bold_magenta", - "yellow", - "bold_yellow", - "red", - "blue", - "bold_blue", -] - -_COLOR_CODES: Final[dict[PrinterColor, str]] = { - "purple": "\033[95m", - "bold_purple": "\033[1m\033[95m", - "red": "\033[91m", - "bold_green": "\033[1m\033[92m", - "green": "\033[32m", - "blue": "\033[94m", - "bold_blue": "\033[1m\033[94m", - "yellow": "\033[93m", - "bold_yellow": "\033[1m\033[93m", - "cyan": "\033[96m", - "bold_cyan": "\033[1m\033[96m", - "magenta": "\033[35m", - "bold_magenta": "\033[1m\033[35m", -} - -RESET: Final[str] = "\033[0m" - - -class ColoredText(NamedTuple): - """Represents text with an optional color for console output. - - Attributes: - text: The text content to be printed. - color: Optional color for the text, specified as a PrinterColor. - """ - - text: str - color: PrinterColor | None - - -class Printer: - """Handles colored console output formatting.""" - - @staticmethod - def print( - content: str | list[ColoredText], - color: PrinterColor | None = None, - sep: str | None = " ", - end: str | None = "\n", - file: SupportsWrite[str] | None = None, - flush: Literal[False] = False, - ) -> None: - """Prints content to the console with optional color formatting. - - Args: - content: Either a string or a list of ColoredText objects for multicolor output. - color: Optional color for the text when content is a string. Ignored when content is a list. - sep: Separator to use between the text and color. - end: String appended after the last value. - file: A file-like object (stream); defaults to the current sys.stdout. - flush: Whether to forcibly flush the stream. - """ - if should_suppress_console_output(): - return - if isinstance(content, str): - content = [ColoredText(content, color)] - print( - "".join( - f"{_COLOR_CODES[c.color] if c.color else ''}{c.text}{RESET}" - for c in content - ), - sep=sep, - end=end, - file=file, - flush=flush, - ) - - -PRINTER: Printer = Printer() +warnings.warn( + "crewai.utilities.printer is deprecated; import from crewai_core.printer.", + DeprecationWarning, + stacklevel=2, +) diff --git a/lib/crewai/src/crewai/utilities/version.py b/lib/crewai/src/crewai/utilities/version.py index 57a5c562d..8acdf9687 100644 --- a/lib/crewai/src/crewai/utilities/version.py +++ b/lib/crewai/src/crewai/utilities/version.py @@ -1,12 +1,14 @@ -"""Version utilities for crewAI.""" +"""Deprecated: use ``crewai_core.version`` instead.""" from __future__ import annotations -from functools import cache -import importlib.metadata +import warnings + +from crewai_core.version import get_crewai_version as get_crewai_version -@cache -def get_crewai_version() -> str: - """Get the installed crewAI version string.""" - return importlib.metadata.version("crewai") +warnings.warn( + "crewai.utilities.version is deprecated; import from crewai_core.version.", + DeprecationWarning, + stacklevel=2, +) diff --git a/lib/crewai/tests/agents/test_agent.py b/lib/crewai/tests/agents/test_agent.py index a21787e25..eae628fce 100644 --- a/lib/crewai/tests/agents/test_agent.py +++ b/lib/crewai/tests/agents/test_agent.py @@ -1225,7 +1225,7 @@ def test_llm_call_with_error(): def test_handle_context_length_exceeds_limit(): # Import necessary modules from crewai.utilities.agent_utils import handle_context_length - from crewai.utilities.printer import Printer + from crewai_core.printer import Printer # Create mocks for dependencies printer = Printer() diff --git a/lib/crewai/tests/agents/test_async_agent_executor.py b/lib/crewai/tests/agents/test_async_agent_executor.py index e4dc7f63f..285005c8f 100644 --- a/lib/crewai/tests/agents/test_async_agent_executor.py +++ b/lib/crewai/tests/agents/test_async_agent_executor.py @@ -405,7 +405,7 @@ class TestAsyncLLMResponseHelper: async def test_aget_llm_response_calls_acall(self) -> None: """Test that aget_llm_response calls llm.acall.""" from crewai.utilities.agent_utils import aget_llm_response - from crewai.utilities.printer import Printer + from crewai_core.printer import Printer mock_llm = MagicMock() mock_llm.acall = AsyncMock(return_value="LLM response") @@ -424,7 +424,7 @@ class TestAsyncLLMResponseHelper: async def test_aget_llm_response_raises_on_empty_response(self) -> None: """Test that aget_llm_response raises ValueError on empty response.""" from crewai.utilities.agent_utils import aget_llm_response - from crewai.utilities.printer import Printer + from crewai_core.printer import Printer mock_llm = MagicMock() mock_llm.acall = AsyncMock(return_value="") @@ -441,7 +441,7 @@ class TestAsyncLLMResponseHelper: async def test_aget_llm_response_propagates_exceptions(self) -> None: """Test that aget_llm_response propagates LLM exceptions.""" from crewai.utilities.agent_utils import aget_llm_response - from crewai.utilities.printer import Printer + from crewai_core.printer import Printer mock_llm = MagicMock() mock_llm.acall = AsyncMock(side_effect=RuntimeError("LLM error")) diff --git a/lib/crewai/tests/memory/test_unified_memory.py b/lib/crewai/tests/memory/test_unified_memory.py index be52e6db5..3c9678b6f 100644 --- a/lib/crewai/tests/memory/test_unified_memory.py +++ b/lib/crewai/tests/memory/test_unified_memory.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock import pytest -from crewai.utilities.printer import Printer +from crewai_core.printer import Printer from crewai.memory.types import ( MemoryConfig, MemoryMatch, diff --git a/lib/crewai/tests/test_checkpoint.py b/lib/crewai/tests/test_checkpoint.py index 525e3ca3b..369db1d6c 100644 --- a/lib/crewai/tests/test_checkpoint.py +++ b/lib/crewai/tests/test_checkpoint.py @@ -206,7 +206,7 @@ class TestRuntimeStateLineage: assert state._branch == "main" def test_serialize_includes_version(self) -> None: - from crewai.utilities.version import get_crewai_version + from crewai_core.version import get_crewai_version state = self._make_state() dumped = json.loads(state.model_dump_json()) diff --git a/lib/crewai/tests/utilities/test_lock_store.py b/lib/crewai/tests/utilities/test_lock_store.py index 8e0e6babc..5ce2d8107 100644 --- a/lib/crewai/tests/utilities/test_lock_store.py +++ b/lib/crewai/tests/utilities/test_lock_store.py @@ -11,8 +11,8 @@ from unittest import mock import pytest -import crewai.utilities.lock_store as lock_store -from crewai.utilities.lock_store import lock +import crewai_core.lock_store as lock_store +from crewai_core.lock_store import lock @pytest.fixture(autouse=True) diff --git a/pyproject.toml b/pyproject.toml index 507541be5..988cb7b28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,6 +112,7 @@ ignore-decorators = ["typing.overload"] "lib/crewai-tools/tests/**/*.py" = ["S101", "RET504", "S105", "S106", "RUF012", "N818", "E402", "RUF043", "S110", "B017"] # Allow various test-specific patterns "lib/crewai-files/tests/**/*.py" = ["S101", "RET504", "S105", "S106", "B017", "F841"] # Allow assert statements and blind exception assertions in tests "lib/cli/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements in tests +"lib/crewai-core/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements in tests "lib/devtools/tests/**/*.py" = ["S101"] @@ -142,6 +143,7 @@ testpaths = [ "lib/crewai-tools/tests", "lib/crewai-files/tests", "lib/cli/tests", + "lib/crewai-core/tests", ] asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" @@ -210,6 +212,7 @@ members = [ "lib/devtools", "lib/crewai-files", "lib/cli", + "lib/crewai-core", ] @@ -219,3 +222,4 @@ crewai-tools = { workspace = true } crewai-devtools = { workspace = true } crewai-files = { workspace = true } crewai-cli = { workspace = true } +crewai-core = { workspace = true } diff --git a/uv.lock b/uv.lock index 63c537269..1a03b6ec4 100644 --- a/uv.lock +++ b/uv.lock @@ -19,6 +19,7 @@ exclude-newer = "2026-04-27T16:00:00Z" members = [ "crewai", "crewai-cli", + "crewai-core", "crewai-devtools", "crewai-files", "crewai-tools", @@ -1280,6 +1281,7 @@ dependencies = [ { name = "appdirs" }, { name = "chromadb" }, { name = "click" }, + { name = "crewai-core" }, { name = "httpx" }, { name = "instructor" }, { name = "json-repair" }, @@ -1387,6 +1389,7 @@ requires-dist = [ { name = "chromadb", specifier = "~=1.1.0" }, { name = "click", specifier = "~=8.1.7" }, { name = "crewai-cli", marker = "extra == 'cli'", editable = "lib/cli" }, + { name = "crewai-core", editable = "lib/crewai-core" }, { name = "crewai-files", marker = "extra == 'file-processing'", editable = "lib/crewai-files" }, { name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" }, { name = "docling", marker = "extra == 'docling'", specifier = "~=2.84.0" }, @@ -1436,11 +1439,10 @@ source = { editable = "lib/cli" } dependencies = [ { name = "appdirs" }, { name = "click" }, - { name = "crewai" }, + { name = "crewai-core" }, { name = "cryptography" }, { name = "httpx" }, { name = "packaging" }, - { name = "portalocker" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyjwt" }, @@ -1455,11 +1457,10 @@ dependencies = [ requires-dist = [ { name = "appdirs", specifier = "~=1.4.4" }, { name = "click", specifier = "~=8.1.7" }, - { name = "crewai", editable = "lib/crewai" }, + { name = "crewai-core", editable = "lib/crewai-core" }, { name = "cryptography", specifier = ">=42.0" }, { name = "httpx", specifier = "~=0.28.1" }, { name = "packaging", specifier = ">=23.0" }, - { name = "portalocker", specifier = "~=2.7.0" }, { name = "pydantic", specifier = ">=2.11.9,<2.13" }, { name = "pydantic-settings", specifier = "~=2.10.1" }, { name = "pyjwt", specifier = ">=2.9.0,<3" }, @@ -1470,6 +1471,28 @@ requires-dist = [ { name = "uv", specifier = "~=0.9.13" }, ] +[[package]] +name = "crewai-core" +source = { editable = "lib/crewai-core" } +dependencies = [ + { name = "appdirs" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "portalocker" }, + { name = "rich" }, +] + +[package.metadata] +requires-dist = [ + { name = "appdirs", specifier = "~=1.4.4" }, + { 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 = "portalocker", specifier = "~=2.7.0" }, + { name = "rich", specifier = ">=13.7.1" }, +] + [[package]] name = "crewai-devtools" source = { editable = "lib/devtools" }