diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index b0d70f388..2990c71af 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "uv~=0.9.13", "aiosqlite~=0.21.0", "lancedb>=0.29.2", + "cloudpickle~=3.1.2", ] [project.urls] diff --git a/lib/crewai/src/crewai/types/callable.py b/lib/crewai/src/crewai/types/callable.py index 500fcfbcd..40204dd97 100644 --- a/lib/crewai/src/crewai/types/callable.py +++ b/lib/crewai/src/crewai/types/callable.py @@ -1,45 +1,35 @@ -"""Serializable callable type for Pydantic models.""" +"""Serializable callable type for Pydantic models. + +All callables (named functions, lambdas, closures, methods) are serialized +via ``cloudpickle`` + base64. On deserialization the base64 payload is +decoded and unpickled back into a live callable. +""" from __future__ import annotations +import base64 from collections.abc import Callable -import importlib from typing import Annotated, Any +import cloudpickle # type: ignore[import-untyped] from pydantic import BeforeValidator, PlainSerializer, WithJsonSchema def _deserialize_callable(v: str | Callable[..., Any]) -> Callable[..., Any]: - """Deserialize a dotted import path to a callable, or pass through if already callable.""" + """Deserialize a base64-encoded cloudpickle payload, or pass through if already callable.""" if isinstance(v, str): - module_path, _, name = v.rpartition(".") - if not module_path: - raise ValueError(f"Invalid callable path: {v!r} (expected 'module.name')") - module = importlib.import_module(module_path) - obj: Callable[..., Any] = getattr(module, name) + obj = cloudpickle.loads(base64.b85decode(v)) if not callable(obj): - raise ValueError(f"{v!r} resolved to {type(obj).__name__}, not a callable") - return obj + raise ValueError( + f"Deserialized object is {type(obj).__name__}, not a callable" + ) + return obj # type: ignore[no-any-return] return v def _serialize_callable(v: Callable[..., Any]) -> str: - """Serialize a callable to its dotted import path.""" - module = getattr(v, "__module__", None) - qualname = getattr(v, "__qualname__", None) - name = getattr(v, "__name__", None) - - if not module or not name: - raise ValueError( - f"Cannot serialize {v!r}: missing __module__ or __name__. " - "Only top-level named functions are serializable." - ) - if qualname and "<" in qualname: - raise ValueError( - f"Cannot serialize {v!r}: lambdas and nested functions are not serializable. " - "Use a top-level named function instead." - ) - return f"{module}.{qualname or name}" + """Serialize any callable to a base64-encoded cloudpickle payload.""" + return base64.b85encode(cloudpickle.dumps(v)).decode("ascii") SerializableCallable = Annotated[ diff --git a/uv.lock b/uv.lock index 8fc9e56f5..0911e10d8 100644 --- a/uv.lock +++ b/uv.lock @@ -911,6 +911,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -1097,6 +1106,7 @@ dependencies = [ { name = "appdirs" }, { name = "chromadb" }, { name = "click" }, + { name = "cloudpickle" }, { name = "httpx" }, { name = "instructor" }, { name = "json-repair" }, @@ -1193,6 +1203,7 @@ requires-dist = [ { name = "boto3", marker = "extra == 'bedrock'", specifier = "~=1.40.45" }, { name = "chromadb", specifier = "~=1.1.0" }, { name = "click", specifier = "~=8.1.7" }, + { name = "cloudpickle", specifier = "~=3.1.2" }, { 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.75.0" },