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: for parent in test_file.parents:
if ( 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" and parent.parent.name == "lib"
): ):
package_root = parent package_root = parent

View File

@@ -1,8 +1,7 @@
# crewai-cli # crewai-cli
CLI for CrewAI — scaffold, run, deploy and manage AI agent crews. CLI for CrewAI — scaffold, run, deploy and manage AI agent crews without
installing the full framework.
The CLI depends on the `crewai` framework and pulls it in automatically.
## Installation ## Installation
@@ -10,7 +9,17 @@ The CLI depends on the `crewai` framework and pulls it in automatically.
pip install crewai-cli 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 ```bash
pip install crewai[cli] pip install crewai[cli]

View File

@@ -8,7 +8,7 @@ authors = [
] ]
requires-python = ">=3.10, <3.14" requires-python = ">=3.10, <3.14"
dependencies = [ dependencies = [
"crewai>=1.14.5a2", "crewai-core>=1.14.5a2",
"click~=8.1.7", "click~=8.1.7",
"pydantic>=2.11.9,<2.13", "pydantic>=2.11.9,<2.13",
"pydantic-settings~=2.10.1", "pydantic-settings~=2.10.1",
@@ -22,7 +22,6 @@ dependencies = [
"packaging>=23.0", "packaging>=23.0",
"python-dotenv>=1.2.2,<2", "python-dotenv>=1.2.2,<2",
"uv~=0.9.13", "uv~=0.9.13",
"portalocker~=2.7.0",
] ]
[project.urls] [project.urls]

View File

@@ -1,7 +1,7 @@
from pathlib import Path from pathlib import Path
import click import click
from crewai.utilities.printer import PRINTER from crewai_core.printer import PRINTER
from crewai_cli.utils import copy_template from crewai_cli.utils import copy_template

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import json import json
from crewai.telemetry.telemetry import Telemetry from crewai_core.telemetry import Telemetry
import httpx import httpx
from rich.console import Console from rich.console import Console

View File

@@ -2,7 +2,7 @@ from pathlib import Path
import shutil import shutil
import click import click
from crewai.telemetry import Telemetry from crewai_core.telemetry import Telemetry
def create_flow(name: str) -> None: def create_flow(name: str) -> None:

View File

@@ -1,7 +1,7 @@
import subprocess import subprocess
import click 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 from crewai_cli.utils import build_env_with_all_tool_credentials

View File

@@ -1,7 +1,7 @@
import subprocess import subprocess
import click 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 from crewai_cli.utils import build_env_with_all_tool_credentials

View File

@@ -2,7 +2,7 @@ from enum import Enum
import subprocess import subprocess
import click 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 packaging import version
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml 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 from typing import Any
import click 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 rich.console import Console
from crewai_cli import git from crewai_cli import git
@@ -33,6 +28,32 @@ from crewai_cli.utils import (
console = Console() 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): class ToolCommand(BaseCommand, PlusAPIMixin):
""" """
A class to handle tool repository related operations for CrewAI projects. A class to handle tool repository related operations for CrewAI projects.
@@ -99,7 +120,8 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
encoded_tarball = None encoded_tarball = None
console.print("[bold blue]Discovering tools from your project...[/bold blue]") 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: if available_exports:
console.print( console.print(
@@ -108,7 +130,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
console.print("[bold blue]Extracting tool metadata...[/bold blue]") console.print("[bold blue]Extracting tool metadata...[/bold blue]")
try: try:
tools_metadata = extract_tools_metadata() tools_metadata = project_utils.extract_tools_metadata()
except Exception as e: except Exception as e:
console.print( console.print(
f"[yellow]Warning: Could not extract tool metadata: {e}[/yellow]\n" 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") console.print(f"Successfully installed {handle}", style="bold green")
def login(self) -> None: def login(self) -> None:
get_user_id = _require_get_user_id()
login_response = self.plus_api_client.login_to_tool_repository( login_response = self.plus_api_client.login_to_tool_repository(
user_identifier=get_user_id() user_identifier=get_user_id()
) )

View File

@@ -1,71 +1,22 @@
"""Standalone user-data helpers for the CLI package. """User-data helpers — re-exported from ``crewai_core.user_data``."""
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.
"""
from __future__ import annotations from __future__ import annotations
import json from crewai_core.paths import db_storage_path as _db_storage_path
import logging from crewai_core.user_data import (
import os _load_user_data as _load_user_data,
from pathlib import Path _save_user_data as _save_user_data,
from typing import Any, cast has_user_declined_tracing as has_user_declined_tracing,
is_tracing_enabled as is_tracing_enabled,
import appdirs update_user_data as update_user_data,
)
logger = logging.getLogger(__name__) __all__ = [
"_db_storage_path",
"_load_user_data",
def _get_project_directory_name() -> str: "_save_user_data",
return os.environ.get("CREWAI_STORAGE_DIR", Path.cwd().name) "has_user_declined_tracing",
"is_tracing_enabled",
"update_user_data",
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"

View File

@@ -10,7 +10,7 @@ from urllib import request
from urllib.error import URLError from urllib.error import URLError
import appdirs 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 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.plus_api.PlusAPI.publish_tool")
@patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=False) @patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=False)
@patch( @patch(
"crewai_cli.tools.main.extract_available_exports", "crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}], return_value=[{"name": "SampleTool"}],
) )
@patch( @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": []}], 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") @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.plus_api.PlusAPI.publish_tool")
@patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=True) @patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=True)
@patch( @patch(
"crewai_cli.tools.main.extract_available_exports", "crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}], return_value=[{"name": "SampleTool"}],
) )
@patch( @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": []}], return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}],
) )
def test_publish_success( def test_publish_success(
@@ -311,11 +311,11 @@ def test_publish_success(
) )
@patch("crewai_cli.plus_api.PlusAPI.publish_tool") @patch("crewai_cli.plus_api.PlusAPI.publish_tool")
@patch( @patch(
"crewai_cli.tools.main.extract_available_exports", "crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}], return_value=[{"name": "SampleTool"}],
) )
@patch( @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": []}], return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}],
) )
def test_publish_failure( def test_publish_failure(
@@ -357,11 +357,11 @@ def test_publish_failure(
) )
@patch("crewai_cli.plus_api.PlusAPI.publish_tool") @patch("crewai_cli.plus_api.PlusAPI.publish_tool")
@patch( @patch(
"crewai_cli.tools.main.extract_available_exports", "crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}], return_value=[{"name": "SampleTool"}],
) )
@patch( @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": []}], 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( 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.plus_api.PlusAPI.publish_tool")
@patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=True) @patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=True)
@patch( @patch(
"crewai_cli.tools.main.extract_available_exports", "crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}], return_value=[{"name": "SampleTool"}],
) )
@patch( @patch(
"crewai_cli.tools.main.extract_tools_metadata", "crewai.utilities.project_utils.extract_tools_metadata",
side_effect=Exception("Failed to extract metadata"), side_effect=Exception("Failed to extract metadata"),
) )
def test_publish_metadata_extraction_failure_continues_with_warning( 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" requires-python = ">=3.10, <3.14"
dependencies = [ dependencies = [
# Shared utilities (version, paths, user_data, telemetry, printer)
"crewai-core",
# Core Dependencies # Core Dependencies
"pydantic>=2.11.9,<2.13", "pydantic>=2.11.9,<2.13",
"openai>=2.30.0,<3", "openai>=2.30.0,<3",

View File

@@ -15,6 +15,7 @@ import inspect
import logging import logging
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
from crewai_core.printer import PRINTER
from pydantic import ( from pydantic import (
AliasChoices, AliasChoices,
BaseModel, BaseModel,
@@ -69,7 +70,6 @@ from crewai.utilities.agent_utils import (
from crewai.utilities.constants import TRAINING_DATA_FILE from crewai.utilities.constants import TRAINING_DATA_FILE
from crewai.utilities.file_store import aget_all_files, get_all_files from crewai.utilities.file_store import aget_all_files, get_all_files
from crewai.utilities.i18n import I18N_DEFAULT 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.string_utils import sanitize_tool_name
from crewai.utilities.token_counter_callback import TokenCalcHandler from crewai.utilities.token_counter_callback import TokenCalcHandler
from crewai.utilities.tool_utils import ( from crewai.utilities.tool_utils import (

View File

@@ -18,6 +18,7 @@ import json
import time import time
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any, cast
from crewai_core.printer import PRINTER
from pydantic import BaseModel from pydantic import BaseModel
from crewai.agents.parser import AgentAction, AgentFinish 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.i18n import I18N_DEFAULT
from crewai.utilities.planning_types import TodoItem 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.step_execution_context import StepExecutionContext, StepResult
from crewai.utilities.string_utils import sanitize_tool_name from crewai.utilities.string_utils import sanitize_tool_name
from crewai.utilities.tool_utils import execute_tool_and_check_finality from crewai.utilities.tool_utils import execute_tool_and_check_finality

View File

@@ -54,6 +54,8 @@ except ImportError:
return [] return []
from crewai_core.printer import PrinterColor
from crewai.agent import Agent from crewai.agent import Agent
from crewai.agents.agent_builder.base_agent import ( from crewai.agents.agent_builder.base_agent import (
BaseAgent, BaseAgent,
@@ -132,7 +134,6 @@ from crewai.utilities.i18n import get_i18n
from crewai.utilities.llm_utils import create_llm from crewai.utilities.llm_utils import create_llm
from crewai.utilities.logger import Logger from crewai.utilities.logger import Logger
from crewai.utilities.planning_handler import CrewPlanner from crewai.utilities.planning_handler import CrewPlanner
from crewai.utilities.printer import PrinterColor
from crewai.utilities.rpm_controller import RPMController from crewai.utilities.rpm_controller import RPMController
from crewai.utilities.streaming import ( from crewai.utilities.streaming import (
create_async_chunk_generator, create_async_chunk_generator,

View File

@@ -15,12 +15,20 @@ from typing import Any, cast
import uuid import uuid
import click 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.console import Console
from rich.panel import Panel from rich.panel import Panel
from rich.text import Text 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 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 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: def on_first_execution_tracing_confirmation() -> bool:
if _is_test_environment(): if _is_test_environment():
return False return False

View File

@@ -3,6 +3,10 @@ import os
import threading import threading
from typing import Any, ClassVar, cast 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.console import Console
from rich.live import Live from rich.live import Live
from rich.panel import Panel from rich.panel import Panel
@@ -15,31 +19,6 @@ _disable_version_check: ContextVar[bool] = ContextVar(
"_disable_version_check", default=False "_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: class ConsoleFormatter:
tool_usage_counts: ClassVar[dict[str, int]] = {} 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 typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
from uuid import uuid4 from uuid import uuid4
from crewai_core.printer import PRINTER
from pydantic import ( from pydantic import (
BaseModel, BaseModel,
Field, Field,
@@ -99,7 +100,6 @@ from crewai.utilities.planning_types import (
TodoItem, TodoItem,
TodoList, TodoList,
) )
from crewai.utilities.printer import PRINTER
from crewai.utilities.step_execution_context import StepExecutionContext, StepResult from crewai.utilities.step_execution_context import StepExecutionContext, StepResult
from crewai.utilities.string_utils import sanitize_tool_name from crewai.utilities.string_utils import sanitize_tool_name
from crewai.utilities.tool_utils import execute_tool_and_check_finality from crewai.utilities.tool_utils import execute_tool_and_check_finality

View File

@@ -30,11 +30,11 @@ import functools
import logging import logging
from typing import TYPE_CHECKING, Any, Final, TypeVar, cast from typing import TYPE_CHECKING, Any, Final, TypeVar, cast
from crewai_core.printer import PRINTER
from pydantic import BaseModel from pydantic import BaseModel
from crewai.flow.persistence.base import FlowPersistence from crewai.flow.persistence.base import FlowPersistence
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
from crewai.utilities.printer import PRINTER
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@@ -9,12 +9,12 @@ from pathlib import Path
import sqlite3 import sqlite3
from typing import TYPE_CHECKING, Any 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 pydantic import BaseModel, Field, PrivateAttr, model_validator
from typing_extensions import Self from typing_extensions import Self
from crewai.flow.persistence.base import FlowPersistence 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: if TYPE_CHECKING:

View File

@@ -22,6 +22,7 @@ import inspect
import textwrap import textwrap
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from crewai_core.printer import PRINTER
from typing_extensions import TypeIs from typing_extensions import TypeIs
from crewai.flow.constants import AND_CONDITION, OR_CONDITION from crewai.flow.constants import AND_CONDITION, OR_CONDITION
@@ -32,7 +33,6 @@ from crewai.flow.flow_wrappers import (
SimpleFlowCondition, SimpleFlowCondition,
) )
from crewai.flow.types import FlowMethodCallable, FlowMethodName from crewai.flow.types import FlowMethodCallable, FlowMethodName
from crewai.utilities.printer import PRINTER
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@@ -2,6 +2,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any, cast
from crewai_core.printer import PRINTER
from crewai.events.event_listener import event_listener from crewai.events.event_listener import event_listener
from crewai.hooks.types import ( from crewai.hooks.types import (
AfterLLMCallHookCallable, AfterLLMCallHookCallable,
@@ -9,7 +11,6 @@ from crewai.hooks.types import (
BeforeLLMCallHookCallable, BeforeLLMCallHookCallable,
BeforeLLMCallHookType, BeforeLLMCallHookType,
) )
from crewai.utilities.printer import PRINTER
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@@ -2,6 +2,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from crewai_core.printer import PRINTER
from crewai.events.event_listener import event_listener from crewai.events.event_listener import event_listener
from crewai.hooks.types import ( from crewai.hooks.types import (
AfterToolCallHookCallable, AfterToolCallHookCallable,
@@ -9,7 +11,6 @@ from crewai.hooks.types import (
BeforeToolCallHookCallable, BeforeToolCallHookCallable,
BeforeToolCallHookType, BeforeToolCallHookType,
) )
from crewai.utilities.printer import PRINTER
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@@ -35,6 +35,8 @@ if TYPE_CHECKING:
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig 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.base_agent import BaseAgent
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
from crewai.agents.cache.cache_handler import CacheHandler 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.guardrail_types import GuardrailCallable, GuardrailType
from crewai.utilities.i18n import I18N_DEFAULT from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.llm_utils import create_llm 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.pydantic_schema_utils import generate_model_description
from crewai.utilities.token_counter_callback import TokenCalcHandler from crewai.utilities.token_counter_callback import TokenCalcHandler
from crewai.utilities.tool_utils import execute_tool_and_check_finality 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: if from_agent is not None:
return True return True
from crewai_core.printer import PRINTER
from crewai.hooks.llm_hooks import ( from crewai.hooks.llm_hooks import (
LLMCallHookContext, LLMCallHookContext,
get_before_llm_call_hooks, get_before_llm_call_hooks,
) )
from crewai.utilities.printer import PRINTER
before_hooks = get_before_llm_call_hooks() before_hooks = get_before_llm_call_hooks()
if not before_hooks: if not before_hooks:
@@ -969,11 +970,12 @@ class BaseLLM(BaseModel, ABC):
if from_agent is not None or not isinstance(response, str): if from_agent is not None or not isinstance(response, str):
return response return response
from crewai_core.printer import PRINTER
from crewai.hooks.llm_hooks import ( from crewai.hooks.llm_hooks import (
LLMCallHookContext, LLMCallHookContext,
get_after_llm_call_hooks, get_after_llm_call_hooks,
) )
from crewai.utilities.printer import PRINTER
after_hooks = get_after_llm_call_hooks() after_hooks = get_after_llm_call_hooks()
if not after_hooks: if not after_hooks:

View File

@@ -5,11 +5,12 @@ from pathlib import Path
import sqlite3 import sqlite3
from typing import Any 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.task import Task
from crewai.utilities.crew_json_encoder import CrewJSONEncoder from crewai.utilities.crew_json_encoder import CrewJSONEncoder
from crewai.utilities.errors import DatabaseError, DatabaseOperationError 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__) logger = logging.getLogger(__name__)

View File

@@ -12,10 +12,10 @@ import threading
import time import time
from typing import Any from typing import Any
from crewai_core.lock_store import lock as store_lock
import lancedb # type: ignore[import-untyped] import lancedb # type: ignore[import-untyped]
from crewai.memory.types import MemoryRecord, ScopeInfo from crewai.memory.types import MemoryRecord, ScopeInfo
from crewai.utilities.lock_store import lock as store_lock
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -68,7 +68,7 @@ class LanceDBStorage:
if storage_dir: if storage_dir:
path = Path(storage_dir) / "memory" path = Path(storage_dir) / "memory"
else: else:
from crewai.utilities.paths import db_storage_path from crewai_core.paths import db_storage_path
path = Path(db_storage_path()) / "memory" path = Path(db_storage_path()) / "memory"
self._path = Path(path) self._path = Path(path)

View File

@@ -104,7 +104,7 @@ class QdrantEdgeStorage:
if storage_dir: if storage_dir:
path = Path(storage_dir) / "memory" / "qdrant-edge" path = Path(storage_dir) / "memory" / "qdrant-edge"
else: 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" path = Path(db_storage_path()) / "memory" / "qdrant-edge"

View File

@@ -10,6 +10,7 @@ from chromadb.api.types import (
EmbeddingFunction as ChromaEmbeddingFunction, EmbeddingFunction as ChromaEmbeddingFunction,
QueryResult, QueryResult,
) )
from crewai_core.lock_store import lock as store_lock
from typing_extensions import Unpack from typing_extensions import Unpack
from crewai.rag.chromadb.types import ( from crewai.rag.chromadb.types import (
@@ -32,7 +33,6 @@ from crewai.rag.core.base_client import (
BaseCollectionParams, BaseCollectionParams,
) )
from crewai.rag.types import SearchResult from crewai.rag.types import SearchResult
from crewai.utilities.lock_store import lock as store_lock
from crewai.utilities.logger_utils import suppress_logging from crewai.utilities.logger_utils import suppress_logging

View File

@@ -3,7 +3,7 @@
import re import re
from typing import Final 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" DEFAULT_TENANT: Final[str] = "default_tenant"

View File

@@ -3,10 +3,10 @@
import os import os
from chromadb import PersistentClient from chromadb import PersistentClient
from crewai_core.lock_store import lock
from crewai.rag.chromadb.client import ChromaDBClient from crewai.rag.chromadb.client import ChromaDBClient
from crewai.rag.chromadb.config import ChromaDBConfig from crewai.rag.chromadb.config import ChromaDBConfig
from crewai.utilities.lock_store import lock
def create_client(config: ChromaDBConfig) -> ChromaDBClient: def create_client(config: ChromaDBConfig) -> ChromaDBClient:

View File

@@ -3,10 +3,10 @@
from typing import Any, cast from typing import Any, cast
from chromadb.api.types import Documents, EmbeddingFunction, Embeddings from chromadb.api.types import Documents, EmbeddingFunction, Embeddings
from crewai_core.printer import PRINTER
from typing_extensions import Unpack from typing_extensions import Unpack
from crewai.rag.embeddings.providers.ibm.types import WatsonXProviderConfig from crewai.rag.embeddings.providers.ibm.types import WatsonXProviderConfig
from crewai.utilities.printer import PRINTER
class WatsonXEmbeddingFunction(EmbeddingFunction[Documents]): class WatsonXEmbeddingFunction(EmbeddingFunction[Documents]):

View File

@@ -3,10 +3,9 @@
import os import os
from typing import Final from typing import Final
from crewai_core.paths import db_storage_path
from qdrant_client.models import Distance, VectorParams 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_VECTOR_PARAMS: Final = VectorParams(size=384, distance=Distance.COSINE)
DEFAULT_EMBEDDING_MODEL: Final[str] = "sentence-transformers/all-MiniLM-L6-v2" 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 from typing import TYPE_CHECKING, Any
import uuid import uuid
from crewai_core.version import get_crewai_version
from packaging.version import Version from packaging.version import Version
from pydantic import ( from pydantic import (
ModelWrapValidatorHandler, ModelWrapValidatorHandler,
@@ -39,7 +40,6 @@ from crewai.state.checkpoint_config import CheckpointConfig
from crewai.state.event_record import EventRecord from crewai.state.event_record import EventRecord
from crewai.state.provider.core import BaseProvider from crewai.state.provider.core import BaseProvider
from crewai.state.provider.json_provider import JsonProvider from crewai.state.provider.json_provider import JsonProvider
from crewai.utilities.version import get_crewai_version
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -77,6 +77,8 @@ except ImportError:
return [] return []
from crewai_core.printer import PRINTER
from crewai.types.callback import SerializableCallable from crewai.types.callback import SerializableCallable
from crewai.utilities.guardrail import ( from crewai.utilities.guardrail import (
process_guardrail, process_guardrail,
@@ -89,7 +91,6 @@ from crewai.utilities.guardrail_types import (
GuardrailsType, GuardrailsType,
) )
from crewai.utilities.i18n import I18N_DEFAULT from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.printer import PRINTER
from crewai.utilities.string_utils import interpolate_only from crewai.utilities.string_utils import interpolate_only

View File

@@ -9,6 +9,7 @@ from textwrap import dedent
import time import time
from typing import TYPE_CHECKING, Any, Literal from typing import TYPE_CHECKING, Any, Literal
from crewai_core.printer import PRINTER
import json5 import json5
from json_repair import repair_json # type: ignore[import-untyped] 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.converter import Converter
from crewai.utilities.i18n import I18N_DEFAULT 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.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.converter import Converter, ConverterError
from crewai.utilities.exceptions.context_window_exceeding_exception import ( from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError, LLMContextLengthExceededError,
@@ -6,7 +8,6 @@ from crewai.utilities.file_handler import FileHandler
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N
from crewai.utilities.internal_instructor import InternalInstructor from crewai.utilities.internal_instructor import InternalInstructor
from crewai.utilities.logger import Logger from crewai.utilities.logger import Logger
from crewai.utilities.printer import Printer
from crewai.utilities.prompts import Prompts from crewai.utilities.prompts import Prompts
from crewai.utilities.rpm_controller import RPMController from crewai.utilities.rpm_controller import RPMController

View File

@@ -12,6 +12,7 @@ import json
import re import re
from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict
from crewai_core.printer import PRINTER, ColoredText, Printer
from pydantic import BaseModel from pydantic import BaseModel
from rich.console import Console from rich.console import Console
@@ -33,7 +34,6 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError, LLMContextLengthExceededError,
) )
from crewai.utilities.i18n import I18N_DEFAULT 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.pydantic_schema_utils import generate_model_description
from crewai.utilities.string_utils import sanitize_tool_name from crewai.utilities.string_utils import sanitize_tool_name
from crewai.utilities.token_counter_callback import TokenCalcHandler from crewai.utilities.token_counter_callback import TokenCalcHandler

View File

@@ -1,15 +1,16 @@
from typing import Annotated, Final 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 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" EMITTER_COLOR: Final[PrinterColor] = "bold_blue"
CC_ENV_VAR: Final[str] = "CLAUDECODE" CC_ENV_VAR: Final[str] = "CLAUDECODE"
CODEX_ENV_VARS: Final[tuple[str, ...]] = ( CODEX_ENV_VARS: Final[tuple[str, ...]] = (

View File

@@ -5,13 +5,13 @@ import json
import re import re
from typing import TYPE_CHECKING, Any, Final, TypedDict from typing import TYPE_CHECKING, Any, Final, TypedDict
from crewai_core.printer import PRINTER
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from typing_extensions import Unpack from typing_extensions import Unpack
from crewai.agents.agent_builder.utilities.base_output_converter import OutputConverter from crewai.agents.agent_builder.utilities.base_output_converter import OutputConverter
from crewai.utilities.i18n import I18N_DEFAULT from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.internal_instructor import InternalInstructor from crewai.utilities.internal_instructor import InternalInstructor
from crewai.utilities.printer import PRINTER
from crewai.utilities.pydantic_schema_utils import generate_model_description from crewai.utilities.pydantic_schema_utils import generate_model_description

View File

@@ -11,6 +11,7 @@ import time
from typing import Any, Final, Literal from typing import Any, Final, Literal
import click import click
from crewai_core.printer import PRINTER
from packaging import version from packaging import version
import tomli import tomli
@@ -19,7 +20,6 @@ from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM from crewai.llms.base_llm import BaseLLM
from crewai.types.crew_chat import ChatInputField, ChatInputs from crewai.types.crew_chat import ChatInputField, ChatInputs
from crewai.utilities.llm_utils import create_llm 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.project_utils import read_toml
from crewai.utilities.types import LLMMessage from crewai.utilities.types import LLMMessage
from crewai.version import get_crewai_version from crewai.version import get_crewai_version

View File

@@ -4,10 +4,9 @@ import os
import pickle import pickle
from typing import Any, TypedDict from typing import Any, TypedDict
from crewai_core.lock_store import lock as store_lock
from typing_extensions import Unpack from typing_extensions import Unpack
from crewai.utilities.lock_store import lock as store_lock
class LogEntry(TypedDict, total=False): class LogEntry(TypedDict, total=False):
"""TypedDict for log entry kwargs with optional fields for flexibility.""" """TypedDict for log entry kwargs with optional fields for flexibility."""

View File

@@ -1,88 +1,14 @@
"""Centralised lock factory. """Deprecated: use ``crewai_core.lock_store`` instead."""
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``.
"""
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterator import warnings
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 from crewai_core.lock_store import lock as lock
import portalocker.exceptions
if TYPE_CHECKING: warnings.warn(
import redis "crewai.utilities.lock_store is deprecated; import from crewai_core.lock_store.",
DeprecationWarning,
stacklevel=2,
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

@@ -1,9 +1,8 @@
from datetime import datetime from datetime import datetime
from crewai_core.printer import PRINTER, ColoredText, PrinterColor
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from crewai.utilities.printer import PRINTER, ColoredText, PrinterColor
class Logger(BaseModel): class Logger(BaseModel):
verbose: bool = Field( 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 __future__ import annotations
from pathlib import Path
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: warnings.warn(
"""Returns the path for SQLite database storage. "crewai.utilities.paths is deprecated; import from crewai_core.paths.",
DeprecationWarning,
Returns: stacklevel=2,
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)

View File

@@ -1,98 +1,19 @@
"""Utility for colored console output.""" """Deprecated: use ``crewai_core.printer`` instead."""
from __future__ import annotations 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: warnings.warn(
from _typeshed import SupportsWrite "crewai.utilities.printer is deprecated; import from crewai_core.printer.",
DeprecationWarning,
PrinterColor = Literal[ stacklevel=2,
"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()

View File

@@ -1,12 +1,14 @@
"""Version utilities for crewAI.""" """Deprecated: use ``crewai_core.version`` instead."""
from __future__ import annotations from __future__ import annotations
from functools import cache import warnings
import importlib.metadata
from crewai_core.version import get_crewai_version as get_crewai_version
@cache warnings.warn(
def get_crewai_version() -> str: "crewai.utilities.version is deprecated; import from crewai_core.version.",
"""Get the installed crewAI version string.""" DeprecationWarning,
return importlib.metadata.version("crewai") stacklevel=2,
)

View File

@@ -1225,7 +1225,7 @@ def test_llm_call_with_error():
def test_handle_context_length_exceeds_limit(): def test_handle_context_length_exceeds_limit():
# Import necessary modules # Import necessary modules
from crewai.utilities.agent_utils import handle_context_length 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 # Create mocks for dependencies
printer = Printer() printer = Printer()

View File

@@ -405,7 +405,7 @@ class TestAsyncLLMResponseHelper:
async def test_aget_llm_response_calls_acall(self) -> None: async def test_aget_llm_response_calls_acall(self) -> None:
"""Test that aget_llm_response calls llm.acall.""" """Test that aget_llm_response calls llm.acall."""
from crewai.utilities.agent_utils import aget_llm_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 = MagicMock()
mock_llm.acall = AsyncMock(return_value="LLM response") 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: async def test_aget_llm_response_raises_on_empty_response(self) -> None:
"""Test that aget_llm_response raises ValueError on empty response.""" """Test that aget_llm_response raises ValueError on empty response."""
from crewai.utilities.agent_utils import aget_llm_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 = MagicMock()
mock_llm.acall = AsyncMock(return_value="") mock_llm.acall = AsyncMock(return_value="")
@@ -441,7 +441,7 @@ class TestAsyncLLMResponseHelper:
async def test_aget_llm_response_propagates_exceptions(self) -> None: async def test_aget_llm_response_propagates_exceptions(self) -> None:
"""Test that aget_llm_response propagates LLM exceptions.""" """Test that aget_llm_response propagates LLM exceptions."""
from crewai.utilities.agent_utils import aget_llm_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 = MagicMock()
mock_llm.acall = AsyncMock(side_effect=RuntimeError("LLM error")) mock_llm.acall = AsyncMock(side_effect=RuntimeError("LLM error"))

View File

@@ -8,7 +8,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from crewai.utilities.printer import Printer from crewai_core.printer import Printer
from crewai.memory.types import ( from crewai.memory.types import (
MemoryConfig, MemoryConfig,
MemoryMatch, MemoryMatch,

View File

@@ -206,7 +206,7 @@ class TestRuntimeStateLineage:
assert state._branch == "main" assert state._branch == "main"
def test_serialize_includes_version(self) -> None: 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() state = self._make_state()
dumped = json.loads(state.model_dump_json()) dumped = json.loads(state.model_dump_json())

View File

@@ -11,8 +11,8 @@ from unittest import mock
import pytest import pytest
import crewai.utilities.lock_store as lock_store import crewai_core.lock_store as lock_store
from crewai.utilities.lock_store import lock from crewai_core.lock_store import lock
@pytest.fixture(autouse=True) @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-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/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/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"] "lib/devtools/tests/**/*.py" = ["S101"]
@@ -142,6 +143,7 @@ testpaths = [
"lib/crewai-tools/tests", "lib/crewai-tools/tests",
"lib/crewai-files/tests", "lib/crewai-files/tests",
"lib/cli/tests", "lib/cli/tests",
"lib/crewai-core/tests",
] ]
asyncio_mode = "strict" asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function" asyncio_default_fixture_loop_scope = "function"
@@ -210,6 +212,7 @@ members = [
"lib/devtools", "lib/devtools",
"lib/crewai-files", "lib/crewai-files",
"lib/cli", "lib/cli",
"lib/crewai-core",
] ]
@@ -219,3 +222,4 @@ crewai-tools = { workspace = true }
crewai-devtools = { workspace = true } crewai-devtools = { workspace = true }
crewai-files = { workspace = true } crewai-files = { workspace = true }
crewai-cli = { 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 = [ members = [
"crewai", "crewai",
"crewai-cli", "crewai-cli",
"crewai-core",
"crewai-devtools", "crewai-devtools",
"crewai-files", "crewai-files",
"crewai-tools", "crewai-tools",
@@ -1280,6 +1281,7 @@ dependencies = [
{ name = "appdirs" }, { name = "appdirs" },
{ name = "chromadb" }, { name = "chromadb" },
{ name = "click" }, { name = "click" },
{ name = "crewai-core" },
{ name = "httpx" }, { name = "httpx" },
{ name = "instructor" }, { name = "instructor" },
{ name = "json-repair" }, { name = "json-repair" },
@@ -1387,6 +1389,7 @@ requires-dist = [
{ name = "chromadb", specifier = "~=1.1.0" }, { name = "chromadb", specifier = "~=1.1.0" },
{ name = "click", specifier = "~=8.1.7" }, { name = "click", specifier = "~=8.1.7" },
{ name = "crewai-cli", marker = "extra == 'cli'", editable = "lib/cli" }, { 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-files", marker = "extra == 'file-processing'", editable = "lib/crewai-files" },
{ name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" }, { name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" },
{ name = "docling", marker = "extra == 'docling'", specifier = "~=2.84.0" }, { name = "docling", marker = "extra == 'docling'", specifier = "~=2.84.0" },
@@ -1436,11 +1439,10 @@ source = { editable = "lib/cli" }
dependencies = [ dependencies = [
{ name = "appdirs" }, { name = "appdirs" },
{ name = "click" }, { name = "click" },
{ name = "crewai" }, { name = "crewai-core" },
{ name = "cryptography" }, { name = "cryptography" },
{ name = "httpx" }, { name = "httpx" },
{ name = "packaging" }, { name = "packaging" },
{ name = "portalocker" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "pyjwt" }, { name = "pyjwt" },
@@ -1455,11 +1457,10 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "appdirs", specifier = "~=1.4.4" }, { name = "appdirs", specifier = "~=1.4.4" },
{ name = "click", specifier = "~=8.1.7" }, { name = "click", specifier = "~=8.1.7" },
{ name = "crewai", editable = "lib/crewai" }, { name = "crewai-core", editable = "lib/crewai-core" },
{ name = "cryptography", specifier = ">=42.0" }, { name = "cryptography", specifier = ">=42.0" },
{ name = "httpx", specifier = "~=0.28.1" }, { name = "httpx", specifier = "~=0.28.1" },
{ name = "packaging", specifier = ">=23.0" }, { name = "packaging", specifier = ">=23.0" },
{ name = "portalocker", specifier = "~=2.7.0" },
{ name = "pydantic", specifier = ">=2.11.9,<2.13" }, { name = "pydantic", specifier = ">=2.11.9,<2.13" },
{ name = "pydantic-settings", specifier = "~=2.10.1" }, { name = "pydantic-settings", specifier = "~=2.10.1" },
{ name = "pyjwt", specifier = ">=2.9.0,<3" }, { name = "pyjwt", specifier = ">=2.9.0,<3" },
@@ -1470,6 +1471,28 @@ requires-dist = [
{ name = "uv", specifier = "~=0.9.13" }, { 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]] [[package]]
name = "crewai-devtools" name = "crewai-devtools"
source = { editable = "lib/devtools" } source = { editable = "lib/devtools" }