Files
crewAI/lib/crewai/src/crewai/utilities/serialization.py
Greyson LaLonde fe2a4b4e40
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
chore: bug fixes and more refactor
Refactor agent executor to delegate human interactions to a provider: add messages and ask_for_human_input properties, implement _invoke_loop and _format_feedback_message, and replace the internal iterative/training feedback logic with a call to get_provider().handle_feedback.

Make LLMGuardrail kickoff coroutine-aware by detecting coroutines and running them via asyncio.run so both sync and async agents are supported.

Make telemetry more robust by safely handling missing task.output (use empty string) and returning early if span is None before setting attributes.

Improve serialization to detect circular references via an _ancestors set, propagate it through recursive calls, and pass exclude/max_depth/_current_depth consistently to prevent infinite recursion and produce stable serializable output.
2026-02-04 21:21:54 -05:00

128 lines
3.7 KiB
Python

from __future__ import annotations
from datetime import date, datetime
import json
from typing import Any, TypeAlias
import uuid
from pydantic import BaseModel
SerializablePrimitive: TypeAlias = str | int | float | bool | None
Serializable: TypeAlias = (
SerializablePrimitive | list["Serializable"] | dict[str, "Serializable"]
)
def to_serializable(
obj: Any,
exclude: set[str] | None = None,
max_depth: int = 5,
_current_depth: int = 0,
_ancestors: set[int] | None = None,
) -> Serializable:
"""Converts a Python object into a JSON-compatible representation.
Supports primitives, datetime objects, collections, dictionaries, and
Pydantic models. Recursion depth is limited to prevent infinite nesting.
Non-convertible objects default to their string representations.
Args:
obj: Object to transform.
exclude: Set of keys to exclude from the result.
max_depth: Maximum recursion depth. Defaults to 5.
_current_depth: Current recursion depth (for internal use).
_ancestors: Set of ancestor object ids for cycle detection (for internal use).
Returns:
Serializable: A JSON-compatible structure.
"""
if _current_depth >= max_depth:
return repr(obj)
if exclude is None:
exclude = set()
if _ancestors is None:
_ancestors = set()
if isinstance(obj, (str, int, float, bool, type(None))):
return obj
if isinstance(obj, uuid.UUID):
return str(obj)
if isinstance(obj, (date, datetime)):
return obj.isoformat()
object_id = id(obj)
if object_id in _ancestors:
return f"<circular_ref:{type(obj).__name__}>"
new_ancestors = _ancestors | {object_id}
if isinstance(obj, (list, tuple, set)):
return [
to_serializable(
item,
exclude=exclude,
max_depth=max_depth,
_current_depth=_current_depth + 1,
_ancestors=new_ancestors,
)
for item in obj
]
if isinstance(obj, dict):
return {
_to_serializable_key(key): to_serializable(
obj=value,
exclude=exclude,
max_depth=max_depth,
_current_depth=_current_depth + 1,
_ancestors=new_ancestors,
)
for key, value in obj.items()
if key not in exclude
}
if isinstance(obj, BaseModel):
try:
return to_serializable(
obj=obj.model_dump(exclude=exclude),
max_depth=max_depth,
_current_depth=_current_depth + 1,
_ancestors=new_ancestors,
)
except Exception:
try:
return {
_to_serializable_key(k): to_serializable(
v,
max_depth=max_depth,
_current_depth=_current_depth + 1,
_ancestors=new_ancestors,
)
for k, v in obj.__dict__.items()
if k not in (exclude or set())
}
except Exception:
return repr(obj)
return repr(obj)
def _to_serializable_key(key: Any) -> str:
if isinstance(key, (str, int)):
return str(key)
return f"key_{id(key)}_{key!r}"
def to_string(obj: Any) -> str | None:
"""Serializes an object into a JSON string.
Args:
obj: Object to serialize.
Returns:
A JSON-formatted string or `None` if empty.
"""
serializable = to_serializable(obj)
if serializable is None:
return None
return json.dumps(serializable)