Skip to content

Commit dba4d7f

Browse files
Merge pull request #868 from Pipelex/release/v0.26.4
Release v0.26.4
2 parents 44149d4 + dbe4a50 commit dba4d7f

6 files changed

Lines changed: 65 additions & 3 deletions

File tree

.badges/tests.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"schemaVersion": 1,
33
"label": "tests",
4-
"message": "4901",
4+
"message": "4902",
55
"color": "blue",
66
"cacheSeconds": 300
77
}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [v0.26.4] - 2026-05-06
4+
5+
### Fixed
6+
7+
- **Temporal storage delivery no longer fails when `working_memory_raw` contains rehydrated Pydantic instances.** `WorkingMemory.dump_for_temporal()` embeds `__class__`/`__module__` markers on `ListContent` items so the receiving workflow can hydrate them. Kajson's Temporal data converter then eagerly rebuilds those dicts back into `BaseModel` instances (e.g. `PageContent` from `Page[]` outputs) on the activity worker that runs delivery — even though `working_memory_raw` is typed as `dict[str, Any]`. `clean_json_content()` previously walked only dicts/lists/scalars and let `BaseModel` instances reach `json.dumps`, which raised `TypeError: Object of type PageContent is not JSON serializable` and aborted `act_deliver`. `clean_json_content` now reduces any `BaseModel` it encounters via `model_dump(serialize_as_any=True)` (the canonical `smart_dump` path) before continuing the recursive walk.
8+
39
## [v0.26.3] - 2026-05-06
410

511
### Fixed

pipelex/tools/misc/json_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def clean_json_content(content: Any) -> Any:
3333
Removes kajson metadata fields (``__class__``, ``__module__``) and converts
3434
non-JSON-native types to their JSON-safe equivalents:
3535
36+
- ``BaseModel`` -> ``model_dump(serialize_as_any=True)`` (then cleaned recursively)
3637
- ``datetime.datetime`` / ``datetime.date`` / ``datetime.time`` -> ISO-format string
3738
- ``Enum`` -> its ``.value``
3839
- ``Decimal`` -> ``float``
@@ -44,6 +45,12 @@ def clean_json_content(content: Any) -> Any:
4445
Returns:
4546
A cleaned copy of *content* that ``json.dumps`` can serialize directly.
4647
"""
48+
if isinstance(content, BaseModel):
49+
# Pydantic models can land inside otherwise plain dicts when kajson's
50+
# Temporal data converter eagerly rehydrates `__class__` markers on the
51+
# receiving worker. Reduce them to dicts via the canonical smart_dump
52+
# path (model_dump(serialize_as_any=True)) before continuing the walk.
53+
return clean_json_content(content.model_dump(serialize_as_any=True))
4754
if isinstance(content, dict):
4855
cleaned: dict[str, Any] = {}
4956
content_dict = cast("dict[str, Any]", content)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pipelex"
3-
version = "0.26.3"
3+
version = "0.26.4"
44
description = "Execute composable AI methods declared in the MTHDS open standard"
55
authors = [{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }]
66
maintainers = [{ name = "Pipelex staff", email = "oss@pipelex.com" }]

tests/unit/pipelex/pipe_run/test_delivery_executor.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,55 @@ async def test_raw_fallback_html_escapes_special_chars(self, mocker: MockerFixtu
251251
json_text = files["main_stuff.json"].data.decode("utf-8")
252252
assert "</pre><script>alert(1)</script><pre>" in json_text
253253

254+
async def test_generate_result_files_with_pydantic_instances_in_raw(self, mocker: MockerFixture) -> None:
255+
"""working_memory_raw can contain hydrated Pydantic instances after Temporal transit.
256+
257+
When `dump_for_temporal()` runs in a child workflow, it embeds `__class__` metadata
258+
on ListContent items so the parent can reconstruct them. Kajson's Temporal data
259+
converter then eagerly rehydrates those dicts back into BaseModel instances on the
260+
activity worker that runs delivery — even though the typed slot is `dict[str, Any]`.
261+
262+
`clean_json_dumps()` does not know how to serialize a Pydantic BaseModel, so the
263+
delivery activity blows up with `TypeError: Object of type PageContent is not JSON
264+
serializable`. This test pins that scenario.
265+
"""
266+
from pipelex.core.stuffs.image_content import ImageContent # noqa: PLC0415
267+
from pipelex.core.stuffs.page_content import PageContent # noqa: PLC0415
268+
from pipelex.core.stuffs.text_and_images_content import TextAndImagesContent # noqa: PLC0415
269+
from pipelex.core.stuffs.text_content import TextContent # noqa: PLC0415
270+
271+
page = PageContent(
272+
text_and_images=TextAndImagesContent(
273+
text=TextContent(text="Page 1 contents"),
274+
images=[ImageContent(url="https://example.com/img.png")],
275+
),
276+
)
277+
278+
mock_output = mocker.MagicMock()
279+
mock_output.working_memory_raw = {
280+
"root": {
281+
"cv_pages": {
282+
"stuff_code": "cv_pages",
283+
"stuff_name": "cv_pages",
284+
"concept": {
285+
"code": "Page",
286+
"domain_code": "native",
287+
"description": "A page",
288+
"structure_class_name": "PageContent",
289+
},
290+
"content": [page],
291+
}
292+
},
293+
"aliases": {},
294+
}
295+
mock_output.graph_spec = None
296+
297+
files = await DeliveryExecutor().generate_result_files(mock_output)
298+
299+
json_text = files["working_memory.json"].data.decode("utf-8")
300+
assert "Page 1 contents" in json_text
301+
assert "https://example.com/img.png" in json_text
302+
254303
async def test_webhook_failure_raises(self, mocker: MockerFixture) -> None:
255304
import httpx # noqa: PLC0415
256305

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)