refactor(core): extract crewai-core for shared utilities and standalone CLI

- New lib/crewai-core/ package: version, paths, constants, lock_store, user_data,
  printer, telemetry. Pure leaf — depends only on appdirs/portalocker/rich/otel.
- crewai now depends on crewai-core; old crewai.utilities.{version,paths,printer,
  lock_store} and the user-data block of events/listeners/tracing/utils.py become
  one-shot DeprecationWarning shims that re-export from crewai_core.
- crewai-cli drops its hard dep on crewai and depends only on crewai-core. CLI
  imports for telemetry/version/printer/constants now point at crewai_core.
- tools/main.py lazy-imports project_utils + get_user_id; the publish/login
  subcommands print a friendly "requires crewai" error if it's missing.
- crewai-cli is now genuinely standalone: 'crewai --help', 'version', 'login',
  'config', 'traces', 'create', 'template' all work without crewai installed.
- 351 CLI tests + 9 crewai-core smoke tests + crewai's full mypy (471 files) clean.
This commit is contained in:
Greyson Lalonde
2026-05-05 12:41:28 +08:00
parent d1934dabc0
commit 60f3df793f
69 changed files with 960 additions and 443 deletions

View File

@@ -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

View File

@@ -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]

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()
)

View File

@@ -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",
]

View File

@@ -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

View File

@@ -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(

View File

@@ -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.

View File

@@ -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"]

View File

@@ -0,0 +1 @@
__version__ = "1.14.5a2"

View File

@@ -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

View File

@@ -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]

View File

@@ -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)

View File

@@ -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()

View File

View File

@@ -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)

View File

@@ -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"

View File

@@ -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"

View File

View File

@@ -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

View File

@@ -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",

View File

@@ -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 (

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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]] = {}

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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__)

View File

@@ -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)

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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:

View File

@@ -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]):

View File

@@ -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"

View File

@@ -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__)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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, ...]] = (

View File

@@ -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

View File

@@ -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

View File

@@ -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."""

View File

@@ -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,
)

View File

@@ -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(

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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()

View File

@@ -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"))

View File

@@ -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,

View File

@@ -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())

View File

@@ -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)

View File

@@ -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 }

31
uv.lock generated
View File

@@ -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" }