mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-05 17:22:36 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
8
lib/crewai-core/README.md
Normal file
8
lib/crewai-core/README.md
Normal 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.
|
||||
32
lib/crewai-core/pyproject.toml
Normal file
32
lib/crewai-core/pyproject.toml
Normal 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"]
|
||||
1
lib/crewai-core/src/crewai_core/__init__.py
Normal file
1
lib/crewai-core/src/crewai_core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = "1.14.5a2"
|
||||
12
lib/crewai-core/src/crewai_core/constants.py
Normal file
12
lib/crewai-core/src/crewai_core/constants.py
Normal 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
|
||||
89
lib/crewai-core/src/crewai_core/lock_store.py
Normal file
89
lib/crewai-core/src/crewai_core/lock_store.py
Normal 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]
|
||||
26
lib/crewai-core/src/crewai_core/paths.py
Normal file
26
lib/crewai-core/src/crewai_core/paths.py
Normal 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)
|
||||
103
lib/crewai-core/src/crewai_core/printer.py
Normal file
103
lib/crewai-core/src/crewai_core/printer.py
Normal 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()
|
||||
0
lib/crewai-core/src/crewai_core/py.typed
Normal file
0
lib/crewai-core/src/crewai_core/py.typed
Normal file
262
lib/crewai-core/src/crewai_core/telemetry.py
Normal file
262
lib/crewai-core/src/crewai_core/telemetry.py
Normal 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)
|
||||
85
lib/crewai-core/src/crewai_core/user_data.py
Normal file
85
lib/crewai-core/src/crewai_core/user_data.py
Normal 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"
|
||||
23
lib/crewai-core/src/crewai_core/version.py
Normal file
23
lib/crewai-core/src/crewai_core/version.py
Normal 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"
|
||||
0
lib/crewai-core/tests/__init__.py
Normal file
0
lib/crewai-core/tests/__init__.py
Normal file
92
lib/crewai-core/tests/test_smoke.py
Normal file
92
lib/crewai-core/tests/test_smoke.py
Normal 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
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]] = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ...]] = (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
31
uv.lock
generated
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user