mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-03 22:19:27 +00:00
fix: serialize guardrail callables as null for JSON checkpointing
This commit is contained in:
@@ -96,7 +96,7 @@ from crewai.utilities.agent_utils import (
|
||||
from crewai.utilities.constants import TRAINED_AGENTS_DATA_FILE, TRAINING_DATA_FILE
|
||||
from crewai.utilities.converter import Converter, ConverterError
|
||||
from crewai.utilities.env import get_env_context
|
||||
from crewai.utilities.guardrail import process_guardrail
|
||||
from crewai.utilities.guardrail import process_guardrail, serialize_guardrail_for_json
|
||||
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
|
||||
from crewai.utilities.i18n import I18N_DEFAULT
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
@@ -285,7 +285,14 @@ class Agent(BaseAgent):
|
||||
default=None,
|
||||
description="The Agent's role to be used from your repository.",
|
||||
)
|
||||
guardrail: GuardrailType | None = Field(
|
||||
guardrail: Annotated[
|
||||
GuardrailType | None,
|
||||
PlainSerializer(
|
||||
serialize_guardrail_for_json,
|
||||
return_type=str | None,
|
||||
when_used="json",
|
||||
),
|
||||
] = Field(
|
||||
default=None,
|
||||
description="Function or string description of a guardrail to validate agent output",
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import time
|
||||
from types import MethodType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Literal,
|
||||
cast,
|
||||
@@ -25,6 +26,7 @@ from pydantic import (
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic.functional_serializers import PlainSerializer
|
||||
from typing_extensions import Self, deprecated
|
||||
|
||||
|
||||
@@ -86,7 +88,7 @@ from crewai.utilities.converter import (
|
||||
Converter,
|
||||
ConverterError,
|
||||
)
|
||||
from crewai.utilities.guardrail import process_guardrail
|
||||
from crewai.utilities.guardrail import process_guardrail, serialize_guardrail_for_json
|
||||
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
|
||||
from crewai.utilities.i18n import I18N_DEFAULT
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
@@ -235,7 +237,14 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
verbose: bool = Field(
|
||||
default=False, description="Whether to print execution details"
|
||||
)
|
||||
guardrail: GuardrailType | None = Field(
|
||||
guardrail: Annotated[
|
||||
GuardrailType | None,
|
||||
PlainSerializer(
|
||||
serialize_guardrail_for_json,
|
||||
return_type=str | None,
|
||||
when_used="json",
|
||||
),
|
||||
] = Field(
|
||||
default=None,
|
||||
description="Function or string description of a guardrail to validate agent output",
|
||||
)
|
||||
|
||||
@@ -76,6 +76,8 @@ except ImportError:
|
||||
from crewai.types.callback import SerializableCallable
|
||||
from crewai.utilities.guardrail import (
|
||||
process_guardrail,
|
||||
serialize_guardrail_for_json,
|
||||
serialize_guardrails_for_json,
|
||||
)
|
||||
from crewai.utilities.guardrail_types import (
|
||||
GuardrailCallable,
|
||||
@@ -235,11 +237,25 @@ class Task(BaseModel):
|
||||
default=None,
|
||||
)
|
||||
processed_by_agents: set[str] = Field(default_factory=set)
|
||||
guardrail: GuardrailType | None = Field(
|
||||
guardrail: Annotated[
|
||||
GuardrailType | None,
|
||||
PlainSerializer(
|
||||
serialize_guardrail_for_json,
|
||||
return_type=str | None,
|
||||
when_used="json",
|
||||
),
|
||||
] = Field(
|
||||
default=None,
|
||||
description="Function or string description of a guardrail to validate task output before proceeding to next task",
|
||||
)
|
||||
guardrails: GuardrailsType | None = Field(
|
||||
guardrails: Annotated[
|
||||
GuardrailsType | None,
|
||||
PlainSerializer(
|
||||
serialize_guardrails_for_json,
|
||||
return_type=list[str] | str | None,
|
||||
when_used="json",
|
||||
),
|
||||
] = Field(
|
||||
default=None,
|
||||
description="List of guardrails to validate task output before proceeding to next task. Also supports a single guardrail function or string description of a guardrail to validate task output before proceeding to next task",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import warnings
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing_extensions import Self
|
||||
@@ -8,6 +9,46 @@ from typing_extensions import Self
|
||||
from crewai.utilities.guardrail_types import GuardrailCallable
|
||||
|
||||
|
||||
def serialize_guardrail_for_json(
|
||||
value: Any, field_name: str = "guardrail"
|
||||
) -> str | None:
|
||||
"""Serialize a single guardrail value for JSON checkpointing.
|
||||
|
||||
String descriptions are preserved; callable references cannot be
|
||||
JSON-serialized and are dropped with a warning so users know the
|
||||
guardrail will not be present after a checkpoint restore.
|
||||
"""
|
||||
if value is None or isinstance(value, str):
|
||||
return value
|
||||
if callable(value):
|
||||
warnings.warn(
|
||||
f"Callable {field_name!r} cannot be JSON-serialized and will be dropped "
|
||||
f"during checkpointing; restored checkpoints will not run this guardrail.",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def serialize_guardrails_for_json(
|
||||
value: Any, field_name: str = "guardrails"
|
||||
) -> list[str] | str | None:
|
||||
"""Serialize a guardrails value (single or sequence) for JSON checkpointing.
|
||||
|
||||
Dropped callables are filtered out of lists rather than emitted as ``None``;
|
||||
a ``None`` entry would fail validation against ``GuardrailCallable | str``
|
||||
on checkpoint restore.
|
||||
"""
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [
|
||||
item
|
||||
for item in (serialize_guardrail_for_json(g, field_name) for g in value)
|
||||
if item is not None
|
||||
]
|
||||
return serialize_guardrail_for_json(value, field_name)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.lite_agent import LiteAgent
|
||||
|
||||
130
lib/crewai/tests/test_guardrail_serialization.py
Normal file
130
lib/crewai/tests/test_guardrail_serialization.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Tests for JSON serialization of guardrail fields on Task, Agent, and LiteAgent.
|
||||
|
||||
Guardrails accept either string descriptions or callables. Callables cannot be
|
||||
JSON-serialized, so the checkpoint path must drop them rather than raise.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai import Agent, Task
|
||||
from crewai.lite_agent import LiteAgent
|
||||
from crewai.utilities.guardrail import (
|
||||
serialize_guardrail_for_json,
|
||||
serialize_guardrails_for_json,
|
||||
)
|
||||
|
||||
|
||||
def _example_guardrail(output):
|
||||
return True, output
|
||||
|
||||
|
||||
def test_serialize_guardrail_preserves_string() -> None:
|
||||
assert serialize_guardrail_for_json("validate output") == "validate output"
|
||||
|
||||
|
||||
def test_serialize_guardrail_returns_none_for_none() -> None:
|
||||
assert serialize_guardrail_for_json(None) is None
|
||||
|
||||
|
||||
def test_serialize_guardrail_drops_callable_with_warning() -> None:
|
||||
with pytest.warns(UserWarning, match="cannot be JSON-serialized"):
|
||||
assert serialize_guardrail_for_json(_example_guardrail) is None
|
||||
|
||||
|
||||
def test_serialize_guardrails_drops_callables_from_list() -> None:
|
||||
with pytest.warns(UserWarning):
|
||||
result = serialize_guardrails_for_json(["check size", _example_guardrail])
|
||||
assert result == ["check size"]
|
||||
|
||||
|
||||
def test_serialize_guardrails_all_callables_returns_empty_list() -> None:
|
||||
with pytest.warns(UserWarning):
|
||||
result = serialize_guardrails_for_json([_example_guardrail, _example_guardrail])
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_serialize_guardrails_handles_single_string() -> None:
|
||||
assert serialize_guardrails_for_json("only check this") == "only check this"
|
||||
|
||||
|
||||
def test_serialize_guardrails_handles_single_callable() -> None:
|
||||
with pytest.warns(UserWarning):
|
||||
assert serialize_guardrails_for_json(_example_guardrail) is None
|
||||
|
||||
|
||||
def test_task_model_dump_json_with_string_guardrail() -> None:
|
||||
agent = Agent(role="r", goal="g", backstory="b")
|
||||
task = Task(
|
||||
description="Do the thing",
|
||||
expected_output="A thing",
|
||||
agent=agent,
|
||||
guardrail="output must be non-empty",
|
||||
)
|
||||
dumped = task.model_dump(mode="json")
|
||||
assert dumped["guardrail"] == "output must be non-empty"
|
||||
|
||||
|
||||
def test_task_model_dump_json_with_callable_guardrail_does_not_raise() -> None:
|
||||
agent = Agent(role="r", goal="g", backstory="b")
|
||||
task = Task(
|
||||
description="Do the thing",
|
||||
expected_output="A thing",
|
||||
agent=agent,
|
||||
guardrail=_example_guardrail,
|
||||
)
|
||||
with pytest.warns(UserWarning, match="cannot be JSON-serialized"):
|
||||
dumped = task.model_dump(mode="json")
|
||||
assert dumped["guardrail"] is None
|
||||
|
||||
|
||||
def test_task_model_dump_json_with_callable_guardrails_list() -> None:
|
||||
agent = Agent(role="r", goal="g", backstory="b")
|
||||
task = Task(
|
||||
description="Do the thing",
|
||||
expected_output="A thing",
|
||||
agent=agent,
|
||||
guardrails=[_example_guardrail, "also check this"],
|
||||
)
|
||||
with pytest.warns(UserWarning):
|
||||
dumped = task.model_dump(mode="json")
|
||||
assert dumped["guardrails"] == ["also check this"]
|
||||
|
||||
|
||||
def test_task_guardrails_round_trip_through_model_validate() -> None:
|
||||
"""Serialized guardrails must round-trip — None entries would fail validation."""
|
||||
agent = Agent(role="r", goal="g", backstory="b")
|
||||
task = Task(
|
||||
description="Do the thing",
|
||||
expected_output="A thing",
|
||||
agent=agent,
|
||||
guardrails=[_example_guardrail, "also check this"],
|
||||
)
|
||||
with pytest.warns(UserWarning):
|
||||
dumped = task.model_dump(mode="json", exclude={"id"})
|
||||
if isinstance(dumped.get("agent"), dict):
|
||||
dumped["agent"].pop("id", None)
|
||||
Task.model_validate(dumped)
|
||||
|
||||
|
||||
def test_agent_model_dump_json_with_callable_guardrail() -> None:
|
||||
agent = Agent(
|
||||
role="r",
|
||||
goal="g",
|
||||
backstory="b",
|
||||
guardrail=_example_guardrail,
|
||||
)
|
||||
with pytest.warns(UserWarning, match="cannot be JSON-serialized"):
|
||||
dumped = agent.model_dump(mode="json")
|
||||
assert dumped["guardrail"] is None
|
||||
|
||||
|
||||
def test_lite_agent_model_dump_json_with_callable_guardrail() -> None:
|
||||
agent = LiteAgent(
|
||||
role="r",
|
||||
goal="g",
|
||||
backstory="b",
|
||||
guardrail=_example_guardrail,
|
||||
)
|
||||
with pytest.warns(UserWarning, match="cannot be JSON-serialized"):
|
||||
dumped = agent.model_dump(mode="json")
|
||||
assert dumped["guardrail"] is None
|
||||
Reference in New Issue
Block a user