Skip to content

Commit 88f24ef

Browse files
authored
fix: ensure serializable raw events attached to LG AGUI events (#355)
1 parent ce67da1 commit 88f24ef

File tree

2 files changed

+69
-18
lines changed

2 files changed

+69
-18
lines changed

typescript-sdk/integrations/langgraph/python/ag_ui_langgraph/agent.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import uuid
22
import json
33
from typing import Optional, List, Any, Union, AsyncGenerator, Generator
4-
from dataclasses import is_dataclass, asdict
5-
from datetime import date, datetime
64

75
from langgraph.graph.state import CompiledStateGraph
86
from langchain.schema import BaseMessage, SystemMessage
@@ -29,7 +27,9 @@
2927
langchain_messages_to_agui,
3028
resolve_reasoning_content,
3129
resolve_message_content,
32-
camel_to_snake
30+
camel_to_snake,
31+
json_safe_stringify,
32+
make_json_safe
3333
)
3434

3535
from ag_ui.core import (
@@ -90,7 +90,12 @@ def __init__(self, *, name: str, graph: CompiledStateGraph, description: Optiona
9090
self.active_step = None
9191

9292
def _dispatch_event(self, event: ProcessedEvents) -> str:
93-
return event # Fallback if no encoder
93+
if event.type == EventType.RAW:
94+
event.event = make_json_safe(event.event)
95+
elif event.raw_event:
96+
event.raw_event = make_json_safe(event.raw_event)
97+
98+
return event
9499

95100
async def run(self, input: RunAgentInput) -> AsyncGenerator[str, None]:
96101
forwarded_props = {}
@@ -224,7 +229,7 @@ async def _handle_stream_events(self, input: RunAgentInput) -> AsyncGenerator[st
224229
CustomEvent(
225230
type=EventType.CUSTOM,
226231
name=LangGraphEventTypes.OnInterrupt.value,
227-
value=json.dumps(interrupt.value, default=make_json_safe) if not isinstance(interrupt.value, str) else interrupt.value,
232+
value=json.dumps(interrupt.value, default=json_safe_stringify) if not isinstance(interrupt.value, str) else interrupt.value,
228233
raw_event=interrupt,
229234
)
230235
)
@@ -735,16 +740,3 @@ def end_step(self):
735740
self.active_run["node_name"] = None
736741
self.active_step = None
737742
return dispatch
738-
739-
def make_json_safe(o):
740-
if is_dataclass(o): # dataclasses like Flight(...)
741-
return asdict(o)
742-
if hasattr(o, "model_dump"): # pydantic v2
743-
return o.model_dump()
744-
if hasattr(o, "dict"): # pydantic v1
745-
return o.dict()
746-
if hasattr(o, "__dict__"): # plain objects
747-
return vars(o)
748-
if isinstance(o, (datetime, date)):
749-
return o.isoformat()
750-
return str(o) # last resort

typescript-sdk/integrations/langgraph/python/ag_ui_langgraph/utils.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
22
import re
33
from typing import List, Any, Dict, Union
4+
from dataclasses import is_dataclass, asdict
5+
from datetime import date, datetime
46

57
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage, ToolMessage
68
from ag_ui.core import (
@@ -177,3 +179,60 @@ def resolve_message_content(content: Any) -> str | None:
177179

178180
def camel_to_snake(name):
179181
return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
182+
183+
def json_safe_stringify(o):
184+
if is_dataclass(o): # dataclasses like Flight(...)
185+
return asdict(o)
186+
if hasattr(o, "model_dump"): # pydantic v2
187+
return o.model_dump()
188+
if hasattr(o, "dict"): # pydantic v1
189+
return o.dict()
190+
if hasattr(o, "__dict__"): # plain objects
191+
return vars(o)
192+
if isinstance(o, (datetime, date)):
193+
return o.isoformat()
194+
return str(o) # last resort
195+
196+
def make_json_safe(value: Any) -> Any:
197+
"""
198+
Recursively convert a value into a JSON-serializable structure.
199+
200+
- Handles Pydantic models via `model_dump`.
201+
- Handles LangChain messages via `to_dict`.
202+
- Recursively walks dicts, lists, and tuples.
203+
- For arbitrary objects, falls back to `__dict__` if available, else `repr()`.
204+
"""
205+
# Pydantic models
206+
if hasattr(value, "model_dump"):
207+
try:
208+
return make_json_safe(value.model_dump(by_alias=True, exclude_none=True))
209+
except Exception:
210+
pass
211+
212+
# LangChain-style objects
213+
if hasattr(value, "to_dict"):
214+
try:
215+
return make_json_safe(value.to_dict())
216+
except Exception:
217+
pass
218+
219+
# Dict
220+
if isinstance(value, dict):
221+
return {key: make_json_safe(sub_value) for key, sub_value in value.items()}
222+
223+
# List / tuple
224+
if isinstance(value, (list, tuple)):
225+
return [make_json_safe(sub_value) for sub_value in value]
226+
227+
# Already JSON safe
228+
if isinstance(value, (str, int, float, bool)) or value is None:
229+
return value
230+
231+
# Arbitrary object: try __dict__ first, fallback to repr
232+
if hasattr(value, "__dict__"):
233+
return {
234+
"__type__": type(value).__name__,
235+
**make_json_safe(value.__dict__),
236+
}
237+
238+
return repr(value)

0 commit comments

Comments
 (0)