fix(checkpoint): drop unroundtrippable callbacks and adapter state
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled

- callable_to_string returns None for lambdas/closures instead of an
  unresolvable dotted path; Crew filters Nones out of restored callback
  lists.
- EventNode.event serializer honors info.mode so mode='json' calls cascade
  properly into nested event payloads.
- RagTool.adapter serializes to None (post-validator rebuilds from
  config); concrete adapters hold runtime state that can't be round-tripped.
This commit is contained in:
Greyson LaLonde
2026-05-25 19:24:02 -07:00
committed by GitHub
parent c3e2001d52
commit 867df0f633
6 changed files with 130 additions and 157 deletions

View File

@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
import os
from typing import Any, Literal, cast
from typing import Annotated, Any, Literal, cast
from crewai.rag.core.base_embeddings_callable import EmbeddingFunction
from crewai.rag.embeddings.factory import build_embedder
@@ -8,10 +8,13 @@ from crewai.rag.embeddings.types import ProviderSpec
from crewai.tools import BaseTool
from pydantic import (
BaseModel,
BeforeValidator,
ConfigDict,
Field,
PlainSerializer,
TypeAdapter,
ValidationError,
WithJsonSchema,
field_validator,
model_validator,
)
@@ -100,6 +103,26 @@ class Adapter(BaseModel, ABC):
"""Add content to the knowledge base."""
def _resolve_adapter(value: Any) -> Any:
"""Validate the ``adapter`` field, returning a placeholder for dict/None input.
Adapter state is not round-tripped; the ``_ensure_adapter`` post-validator
rebuilds a fresh adapter from the tool's ``config``.
"""
if isinstance(value, Adapter):
return value
if value is None or isinstance(value, dict):
return RagTool._AdapterPlaceholder()
return value
def _serialize_adapter(adapter: Any, info: Any) -> Any:
"""Serialize the ``adapter`` field, dropping runtime state from the payload."""
if not isinstance(adapter, Adapter):
return adapter
return None
class RagTool(BaseTool):
class _AdapterPlaceholder(Adapter):
def query(
@@ -123,7 +146,12 @@ class RagTool(BaseTool):
similarity_threshold: float = 0.6
limit: int = 5
collection_name: str = "rag_tool_collection"
adapter: Adapter = Field(default_factory=_AdapterPlaceholder)
adapter: Annotated[
Adapter,
BeforeValidator(_resolve_adapter),
PlainSerializer(_serialize_adapter, when_used="json"),
WithJsonSchema({"type": ["object", "null"]}),
] = Field(default_factory=_AdapterPlaceholder)
config: RagToolConfig = Field(
default_factory=RagToolConfig,
description="Configuration format accepted by RagTool.",

View File

@@ -2912,12 +2912,6 @@
"humanized_name": "Search a CSV's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -3903,7 +3897,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -3964,12 +3961,6 @@
"humanized_name": "Search a Code Docs content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -4955,7 +4946,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -5641,12 +5635,6 @@
"humanized_name": "Search a DOCX's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -6632,7 +6620,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -7926,12 +7917,6 @@
"humanized_name": "Search a directory's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -8917,7 +8902,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -10762,12 +10750,6 @@
"humanized_name": "Search a github repo's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -11753,7 +11735,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -12041,12 +12026,6 @@
"humanized_name": "Search a JSON's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -13032,7 +13011,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -13316,12 +13298,6 @@
"humanized_name": "Search a MDX's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -14307,7 +14283,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -14774,12 +14753,6 @@
"humanized_name": "Search a database's table content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -15765,7 +15738,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -15967,21 +15943,6 @@
"title": "EnvVar",
"type": "object"
},
"JsonResponseFormat": {
"description": "Response format requesting raw JSON output (e.g. ``{\"type\": \"json_object\"}``).",
"properties": {
"type": {
"const": "json_object",
"title": "Type",
"type": "string"
}
},
"required": [
"type"
],
"title": "JsonResponseFormat",
"type": "object"
},
"LLM": {
"properties": {
"additional_params": {
@@ -16210,16 +16171,6 @@
"title": "Reasoning Effort"
},
"response_format": {
"anyOf": [
{
"$ref": "#/$defs/JsonResponseFormat"
},
{},
{
"type": "null"
}
],
"default": null,
"title": "Response Format"
},
"seed": {
@@ -17207,12 +17158,6 @@
"humanized_name": "Search a PDF's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -18198,7 +18143,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -18906,12 +18854,6 @@
"humanized_name": "Knowledge base",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -19897,7 +19839,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -20994,12 +20939,6 @@
"humanized_name": "Job Search",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -21985,7 +21924,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -22462,12 +22404,6 @@
"humanized_name": "Webpage to Markdown",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -23453,7 +23389,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -24307,12 +24246,6 @@
"humanized_name": "Search a txt's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -25298,7 +25231,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -26227,12 +26163,6 @@
"humanized_name": "Search in a specific website",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -27218,7 +27148,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -27279,12 +27212,6 @@
"humanized_name": "Search a XML's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -28270,7 +28197,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -28331,12 +28261,6 @@
"humanized_name": "Search a Youtube Channels content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -29322,7 +29246,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",
@@ -29383,12 +29310,6 @@
"humanized_name": "Search a Youtube Video content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -30374,7 +30295,10 @@
},
"properties": {
"adapter": {
"$ref": "#/$defs/Adapter"
"type": [
"object",
"null"
]
},
"collection_name": {
"default": "rag_tool_collection",

View File

@@ -382,6 +382,15 @@ class Crew(FlowTrackable, BaseModel):
checkpoint_train: bool | None = Field(default=None)
checkpoint_kickoff_event_id: str | None = Field(default=None)
@field_validator(
"before_kickoff_callbacks", "after_kickoff_callbacks", mode="before"
)
@classmethod
def _drop_unresolvable_callbacks(cls, value: Any) -> Any:
if isinstance(value, list):
return [v for v in value if v is not None]
return value
@classmethod
def from_checkpoint(cls, config: CheckpointConfig) -> Crew:
"""Restore a Crew from a checkpoint, ready to resume via kickoff().

View File

@@ -67,7 +67,11 @@ class EventNode(BaseModel):
event: Annotated[
BaseEvent,
BeforeValidator(_resolve_event),
PlainSerializer(lambda v: v.model_dump()),
PlainSerializer(
lambda v, info: (
v.model_dump(mode="json") if info.mode == "json" else v.model_dump()
),
),
]
edges: dict[EdgeType, list[str]] = Field(default_factory=dict)

View File

@@ -130,18 +130,15 @@ def _resolve_dotted_path(path: str) -> Callable[..., Any]:
raise ValueError(f"Cannot resolve callback {path!r}")
def callable_to_string(fn: Callable[..., Any]) -> str:
"""Serialize a callable to its dotted-path string representation.
Uses ``fn.__module__`` and ``fn.__qualname__`` to produce a string such
as ``"builtins.print"``. Lambdas and closures produce paths that contain
``<locals>`` and cannot be round-tripped via :func:`string_to_callable`.
def callable_to_string(fn: Callable[..., Any]) -> str | None:
"""Serialize a module-level callable as a ``"module.qualname"`` string.
Args:
fn: The callable to serialize.
Returns:
A dotted string of the form ``"module.qualname"``.
The dotted path, or ``None`` for lambdas and closures (not
resolvable by :func:`string_to_callable`).
"""
module = getattr(fn, "__module__", None)
qualname = getattr(fn, "__qualname__", None)
@@ -150,6 +147,8 @@ def callable_to_string(fn: Callable[..., Any]) -> str:
f"Cannot serialize {fn!r}: missing __module__ or __qualname__. "
"Use a module-level named function for checkpointable callbacks."
)
if "<locals>" in qualname or qualname == "<lambda>":
return None
return f"{module}.{qualname}"

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import functools
import os
from collections.abc import Callable
from typing import Any
import pytest
from pydantic import BaseModel, ValidationError
@@ -93,10 +94,18 @@ class TestCallableToString:
result = callable_to_string(print)
assert result == "builtins.print"
def test_lambda_produces_locals_path(self) -> None:
def test_lambda_returns_none(self) -> None:
fn = lambda: None # noqa: E731
result = callable_to_string(fn)
assert "<lambda>" in result
assert callable_to_string(fn) is None
def test_closure_returns_none(self) -> None:
def outer() -> Callable[[], None]:
def inner() -> None:
return None
return inner
assert callable_to_string(outer()) is None
def test_missing_qualname_raises(self) -> None:
obj = type("NoQual", (), {"__module__": "test"})()