Skip to content

Commit 7406142

Browse files
committed
use safe json in langgraph python integration
1 parent df8c661 commit 7406142

File tree

5 files changed

+72
-78
lines changed

5 files changed

+72
-78
lines changed

python-sdk/ag_ui/core/utils.py

Lines changed: 0 additions & 45 deletions
This file was deleted.

python-sdk/ag_ui/encoder/encoder.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""
22
This module contains the EventEncoder class
33
"""
4-
import json
4+
55
from ag_ui.core.events import BaseEvent
6-
from ag_ui.core.utils import make_json_safe
76

87
AGUI_MEDIA_TYPE = "application/vnd.ag-ui.event+proto"
98

@@ -30,7 +29,4 @@ def _encode_sse(self, event: BaseEvent) -> str:
3029
"""
3130
Encodes an event into an SSE string.
3231
"""
33-
event_dict = event.model_dump(by_alias=True, exclude_none=True)
34-
json_ready = make_json_safe(event_dict)
35-
json_string = json.dumps(json_ready)
36-
return f"data: {json_string}\n\n"
32+
return f"data: {event.model_dump_json(by_alias=True, exclude_none=True)}\n\n"

python-sdk/tests/test_encoder.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from ag_ui.encoder.encoder import EventEncoder, AGUI_MEDIA_TYPE
66
from ag_ui.core.events import BaseEvent, EventType, TextMessageContentEvent, ToolCallStartEvent
7-
from ag_ui.core.utils import make_json_safe
7+
88

99
class TestEventEncoder(unittest.TestCase):
1010
"""Test suite for EventEncoder class"""
@@ -29,10 +29,7 @@ def test_encode_method(self):
2929
encoded = encoder.encode(event)
3030

3131
# The encode method calls encode_sse, so the result should be in SSE format
32-
event_dict = event.model_dump(by_alias=True, exclude_none=True)
33-
json_ready = make_json_safe(event_dict)
34-
json_string = json.dumps(json_ready)
35-
expected = f"data: {json_string}\n\n"
32+
expected = f"data: {event.model_dump_json(by_alias=True, exclude_none=True)}\n\n"
3633
self.assertEqual(encoded, expected)
3734

3835
# Verify that camelCase is used in the encoded output
@@ -81,7 +78,7 @@ def test_encode_with_different_event_types(self):
8178
# Test with a basic BaseEvent
8279
base_event = BaseEvent(type=EventType.RAW, timestamp=1648214400000)
8380
encoded_base = encoder.encode(base_event)
84-
self.assertIn('"type": "RAW"', encoded_base)
81+
self.assertIn('"type":"RAW"', encoded_base)
8582

8683
# Test with a more complex event
8784
content_event = TextMessageContentEvent(
@@ -92,9 +89,9 @@ def test_encode_with_different_event_types(self):
9289
encoded_content = encoder.encode(content_event)
9390

9491
# Verify correct encoding and camelCase conversion
95-
self.assertIn('"type": "TEXT_MESSAGE_CONTENT"', encoded_content)
96-
self.assertIn('"messageId": "msg_456"', encoded_content) # Check snake_case converted to camelCase
97-
self.assertIn('"delta": "Testing different events"', encoded_content)
92+
self.assertIn('"type":"TEXT_MESSAGE_CONTENT"', encoded_content)
93+
self.assertIn('"messageId":"msg_456"', encoded_content) # Check snake_case converted to camelCase
94+
self.assertIn('"delta":"Testing different events"', encoded_content)
9895

9996
# Extract JSON and verify camelCase conversion
10097
json_content = encoded_content.split("data: ")[1].rstrip("\n\n")

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

Lines changed: 5 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,7 @@ 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+
return make_json_safe(event) # Fallback if no encoder
9494

9595
async def run(self, input: RunAgentInput) -> AsyncGenerator[str, None]:
9696
forwarded_props = {}
@@ -224,7 +224,7 @@ async def _handle_stream_events(self, input: RunAgentInput) -> AsyncGenerator[st
224224
CustomEvent(
225225
type=EventType.CUSTOM,
226226
name=LangGraphEventTypes.OnInterrupt.value,
227-
value=json.dumps(interrupt.value, default=make_json_safe) if not isinstance(interrupt.value, str) else interrupt.value,
227+
value=json.dumps(interrupt.value, default=json_safe_stringify) if not isinstance(interrupt.value, str) else interrupt.value,
228228
raw_event=interrupt,
229229
)
230230
)
@@ -735,16 +735,3 @@ def end_step(self):
735735
self.active_run["node_name"] = None
736736
self.active_step = None
737737
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)