Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 152 additions & 10 deletions docs/modules/serialization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,33 @@ BeeAI framework provides robust serialization capabilities through its built-in
- 📦 Snapshots: Create point-in-time captures of component state
- 🔧 Reconstruction: Rebuild objects from their serialized representation

The Python example prints the serialized payload, the restored ISO timestamp, and a boolean confirming the round trip.

<CodeGroup>

{/* <!-- comingsoon python/examples/serialization/base.py --> */}
{/* <!-- embedme python/examples/serialization/base.py --> */}
```py Python [expandable]
Example coming soon
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

from datetime import UTC, datetime

from beeai_framework.serialization import Serializer


def main() -> None:
original = datetime(2024, 1, 1, tzinfo=UTC)
serialized = Serializer.serialize(original)
restored = Serializer.deserialize(serialized, expected_type=datetime)

print(serialized)
print(restored.isoformat())
print(restored == original)


if __name__ == "__main__":
main()

```

{/* <!-- embedme typescript/examples/serialization/base.ts --> */}
Expand Down Expand Up @@ -69,11 +91,37 @@ The serialization process involves:

Most BeeAI components can be serialized out of the box. Here's an example using memory:

Running the Python snippet restores the memory, appends an assistant reply, and prints the message count plus the first question.

<CodeGroup>

{/* <!-- comingsoon python/examples/serialization/memory.py --> */}
{/* <!-- embedme python/examples/serialization/memory.py --> */}
```py Python [expandable]
Example coming soon
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

import asyncio

from beeai_framework.backend import AssistantMessage, UserMessage
from beeai_framework.memory import UnconstrainedMemory


async def main() -> None:
memory = UnconstrainedMemory()
await memory.add(UserMessage("What is your name?"))

serialized = memory.serialize()
restored = UnconstrainedMemory.from_serialized(serialized)

await restored.add(AssistantMessage("Bee"))

print(len(restored.messages))
print(restored.messages[0].text)


if __name__ == "__main__":
asyncio.run(main())

```

{/* <!-- embedme typescript/examples/serialization/memory.ts --> */}
Expand Down Expand Up @@ -107,11 +155,52 @@ If you want to serialize a class that the `Serializer` does not know, you may re

You can register external classes with the serializer:

This example registers a lightweight data class and prints the reconstructed token along with its expiry timestamp.

<CodeGroup>

{/* <!-- comingsoon python/examples/serialization/custom_external.py --> */}
{/* <!-- embedme python/examples/serialization/custom_external.py --> */}
```py Python [expandable]
Example coming soon
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

from dataclasses import dataclass
from datetime import UTC, datetime

from beeai_framework.serialization import Serializer


@dataclass
class ApiToken:
value: str
expires_at: datetime


Serializer.register(
ApiToken,
to_plain=lambda token: {
"value": token.value,
"expires_at": token.expires_at,
},
from_plain=lambda payload: ApiToken(
value=payload["value"],
expires_at=payload["expires_at"],
),
)


def main() -> None:
token = ApiToken("example-token", datetime(2025, 1, 1, tzinfo=UTC))
serialized = Serializer.serialize(token)
restored = Serializer.deserialize(serialized, expected_type=ApiToken)

print(restored)
print(restored.expires_at.isoformat())


if __name__ == "__main__":
main()

```

{/* <!-- embedme typescript/examples/serialization/customExternal.ts --> */}
Expand Down Expand Up @@ -150,11 +239,46 @@ console.info(deserialized);

For deeper integration, extend the Serializable class:

The Python variant increments a counter, serializes it, and prints both the live and restored values.

<CodeGroup>

{/* <!-- comingsoon python/examples/serialization/custom_internal.py --> */}
{/* <!-- embedme python/examples/serialization/custom_internal.py --> */}
```py Python [expandable]
Example coming soon
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

from beeai_framework.serialization import Serializable


class Counter(Serializable[dict[str, int]]):
def __init__(self, value: int = 0) -> None:
self.value = value

def increment(self) -> None:
self.value += 1

def create_snapshot(self) -> dict[str, int]:
return {"value": self.value}

def load_snapshot(self, snapshot: dict[str, int]) -> None:
self.value = snapshot["value"]


def main() -> None:
counter = Counter(3)
counter.increment()

serialized = counter.serialize()
restored = Counter.from_serialized(serialized)

print(counter.value)
print(restored.value)


if __name__ == "__main__":
main()

```

{/* <!-- embedme typescript/examples/serialization/customInternal.ts --> */}
Expand Down Expand Up @@ -199,11 +323,29 @@ Failure to register a class that the `Serializer` does not know will result in t

## Context matters

Deserialize with the `extraClasses` equivalent to make sure message factories are registered; the Python snippet imports both `UserMessage` and `AssistantMessage` so the serializer can hydrate their content and then prints every restored user utterance.

<CodeGroup>

{/* <!-- comingsoon python/examples/serialization/context.py --> */}
{/* <!-- embedme python/examples/serialization/context.py --> */}
```py Python [expandable]
Example coming soon
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

from beeai_framework.backend import AssistantMessage, UserMessage
from beeai_framework.memory import UnconstrainedMemory

SERIALIZED_MEMORY = """{"__version":"0.0.0","__root":{"__serializer":true,"__class":"UnconstrainedMemory","__value":{"messages":[{"__serializer":true,"__class":"UserMessage","__value":{"id":null,"meta":{"createdAt":{"__serializer":true,"__class":"datetime","__value":"2025-10-18T19:38:37.859543+00:00"}},"role":"user","content":[{"type":"text","text":"Hello!"}]}},{"__serializer":true,"__class":"AssistantMessage","__value":{"id":null,"meta":{"createdAt":{"__serializer":true,"__class":"datetime","__value":"2025-10-18T19:38:37.859665+00:00"}},"role":"assistant","content":[{"type":"text","text":"Hello, how can I help you?"}]}}]}}}"""


def main() -> None:
memory = UnconstrainedMemory.from_serialized(SERIALIZED_MEMORY, extra_classes=[UserMessage, AssistantMessage])
print([message.text for message in memory.messages])


if __name__ == "__main__":
main()

```

{/* <!-- embedme typescript/examples/serialization/context.ts --> */}
Expand Down
61 changes: 53 additions & 8 deletions python/beeai_framework/backend/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import enum
import json
from abc import ABC
from collections.abc import Sequence
from datetime import UTC, datetime
from enum import Enum
Expand All @@ -12,6 +11,7 @@
from pydantic import BaseModel, ConfigDict, Field, computed_field
from typing_extensions import TypedDict

from beeai_framework.serialization import Serializable
from beeai_framework.utils.dicts import exclude_none
from beeai_framework.utils.lists import cast_list
from beeai_framework.utils.models import to_any_model, to_model
Expand All @@ -36,7 +36,25 @@ def values(cls) -> set[str]:
return {value for key, value in vars(cls).items() if not key.startswith("_") and isinstance(value, str)}


class MessageTextContent(BaseModel):
class SerializableModel(BaseModel, Serializable[dict[str, Any]], auto_register=False):
"""Base class for Pydantic models that need serialization support.

This class combines BaseModel with Serializable to provide consistent
serialization behavior for message content parts.
"""

def create_snapshot(self) -> dict[str, Any]:
"""Create a snapshot of this model for serialization."""
return self.model_dump()

def load_snapshot(self, snapshot: dict[str, Any]) -> None:
"""Load state from a snapshot dictionary."""
for field_name, value in snapshot.items():
object.__setattr__(self, field_name, value)
object.__setattr__(self, "__pydantic_fields_set__", set(snapshot.keys()))


class MessageTextContent(SerializableModel):
type: Literal["text"] = "text"
text: str

Expand All @@ -47,12 +65,12 @@ class MessageImageContentImageUrl(TypedDict, total=False):
format: str


class MessageImageContent(BaseModel):
class MessageImageContent(SerializableModel):
type: Literal["image_url"] = "image_url"
image_url: MessageImageContentImageUrl


class MessageFileContent(BaseModel):
class MessageFileContent(SerializableModel):
"""File content part (e.g. PDF or other document) for multimodal user messages.

Flattened shape is supported:
Expand All @@ -78,15 +96,25 @@ def model_post_init(self, __context: Any) -> None:
if not (self.file_id or self.file_data):
raise ValueError("Either 'file_id' or 'file_data' must be provided for MessageFileContent")

def create_snapshot(self) -> dict[str, Any]:
"""Create snapshot including excluded fields for proper serialization."""
return {
"type": self.type,
"file_id": self.file_id,
"file_data": self.file_data,
"filename": self.filename,
"format": self.format,
}


class MessageToolResultContent(BaseModel):
class MessageToolResultContent(SerializableModel):
type: Literal["tool-result"] = "tool-result"
result: Any
tool_name: str
tool_call_id: str


class MessageToolCallContent(BaseModel):
class MessageToolCallContent(SerializableModel):
type: Literal["tool-call"] = "tool-call"
id: str
tool_name: str
Expand All @@ -103,7 +131,7 @@ def is_valid(self) -> bool:
return False


class Message(ABC, Generic[T]):
class Message(Serializable[dict[str, Any]], Generic[T]):
id: str | None
role: Role | str
content: list[T]
Expand Down Expand Up @@ -152,6 +180,23 @@ def __str__(self) -> str:
def clone(self) -> Self:
return type(self)([c.model_copy() for c in self.content], self.meta.copy())

def create_snapshot(self) -> dict[str, Any]:
"""Create a snapshot of this message for serialization."""
return {
"id": self.id,
"meta": dict(self.meta),
"role": str(self.role),
# Return content as-is - Serializer will handle encoding each item
"content": list(self.content),
}

def load_snapshot(self, snapshot: dict[str, Any]) -> None:
"""Load state from a snapshot dictionary."""
self.id = snapshot.get("id")
self.meta = dict(snapshot.get("meta") or {})
# Content is already deserialized by the Serializer as proper objects
self.content = snapshot.get("content", [])


AssistantMessageContent = MessageTextContent | MessageToolCallContent

Expand Down Expand Up @@ -314,7 +359,7 @@ def from_text(cls, text: str) -> Self:
return cls(MessageTextContent(text=text))


class CustomMessageContent(BaseModel):
class CustomMessageContent(SerializableModel):
model_config = ConfigDict(extra="allow")


Expand Down
Loading