deerflow-code/offline-backend-20260512/backend/packages/harness/deerflow/runtime/serialization.py
2026-09-07 18:24:55 +08:00

99 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Canonical serialization for LangChain / LangGraph objects.
Provides a single source of truth for converting LangChain message
objects, Pydantic models, and LangGraph state dicts into plain
JSON-serialisable Python structures.
Consumers: ``deerflow.runtime.runs.worker`` (SSE publishing) and
``app.gateway.routers.threads`` (REST responses).
"""
from __future__ import annotations
import dataclasses
from typing import Any
def serialize_lc_object(obj: Any) -> Any:
"""Recursively serialize a LangChain object to a JSON-serialisable dict."""
if obj is None:
return None
if isinstance(obj, (str, int, float, bool)):
return obj
if isinstance(obj, dict):
return {k: serialize_lc_object(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [serialize_lc_object(item) for item in obj]
# Pydantic v2
if hasattr(obj, "model_dump"):
try:
return obj.model_dump()
except Exception:
pass
# Pydantic v1 / older objects
if hasattr(obj, "dict"):
try:
return obj.dict()
except Exception:
pass
# Dataclass instances —— LangGraph 的 ``Interrupt`` 就是 dataclass 没有
# model_dump 也没有 dict 方法;之前 fallback 到 ``str(obj)`` 会把
# ``Interrupt(value=..., id='...')`` 序列化成字符串,下游 SDK
# (``@langchain/langgraph-sdk``) 期待 dict 结构 → 直接丢弃 → AI 写作
# 的 4 个干预卡永远不弹(参 ai-writing-优化.md §阶段 3 排查记录)。
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
try:
return {k: serialize_lc_object(v) for k, v in dataclasses.asdict(obj).items()}
except Exception:
pass
# Last resort
try:
return str(obj)
except Exception:
return repr(obj)
def serialize_channel_values(channel_values: dict[str, Any]) -> dict[str, Any]:
"""Serialize channel values, stripping internal LangGraph keys.
``__pregel_*`` 键始终剥离。``__interrupt__`` 仅在**为空时**剥离 —— 因为
``@langchain/langgraph-sdk`` 的 ``useStream`` 只在 ``event === "values"``
通道里读 ``data.__interrupt__`` 并通过 ``stream.interrupt`` 暴露给上层。
AI 写作的 4 个干预卡(material/outline/draft/review)就是靠它触发的;
继续过滤会让前端永远拿不到 interrupt。
主聊天不消费 ``stream.interrupt``,但若我们让一个空数组溜过去,SDK 会
fallback 成 ``{when: "breakpoint"}`` 的伪 interrupt —— 所以仅在数组非空
时保留,两侧都不破坏。
"""
result: dict[str, Any] = {}
for key, value in channel_values.items():
if key.startswith("__pregel_"):
continue
if key == "__interrupt__" and not value:
continue
result[key] = serialize_lc_object(value)
return result
def serialize_messages_tuple(obj: Any) -> Any:
"""Serialize a messages-mode tuple ``(chunk, metadata)``."""
if isinstance(obj, tuple) and len(obj) == 2:
chunk, metadata = obj
return [serialize_lc_object(chunk), metadata if isinstance(metadata, dict) else {}]
return serialize_lc_object(obj)
def serialize(obj: Any, *, mode: str = "") -> Any:
"""Serialize LangChain objects with mode-specific handling.
* ``messages`` — obj is ``(message_chunk, metadata_dict)``
* ``values`` — obj is the full state dict; ``__pregel_*`` keys stripped
* everything else — recursive ``model_dump()`` / ``dict()`` fallback
"""
if mode == "messages":
return serialize_messages_tuple(obj)
if mode == "values":
return serialize_channel_values(obj) if isinstance(obj, dict) else serialize_lc_object(obj)
return serialize_lc_object(obj)