Skip to content

Commit d6ff577

Browse files
authored
fix(sdk-python): LangGraphAGUIAgent serializes Context objects to dicts (CopilotKit#3690) (CopilotKit#3787)
## Summary - `LangGraphAGUIAgent.langgraph_default_merge_state` now calls `model_dump()` on Pydantic Context objects before storing in copilotkit state - Previously stored raw Pydantic objects, causing JSON serialization failures downstream - Handles mixed types: Pydantic objects get `model_dump()`, plain dicts pass through unchanged - Matches the existing pattern already used in `CopilotKitMiddleware` ## Test plan - [x] Red-green test: AG-UI Context objects stored as plain dicts, not Pydantic - [x] Red-green test: mixed Pydantic + dict context items all serializable - [x] Full test suite passes (15/15) Closes CopilotKit#3690
2 parents b28d153 + e01d510 commit d6ff577

2 files changed

Lines changed: 73 additions & 2 deletions

File tree

sdk-python/copilotkit/langgraph_agui_agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ def langgraph_default_merge_state(self, state: State, messages: List[BaseMessage
198198
return {
199199
**merged_state,
200200
'copilotkit': {
201-
'actions': agui_properties.get('tools', []),
202-
'context': agui_properties.get('context', [])
201+
'actions': [a.model_dump() if hasattr(a, 'model_dump') else a for a in agui_properties.get('tools', [])],
202+
'context': [c.model_dump() if hasattr(c, 'model_dump') else c for c in agui_properties.get('context', [])]
203203
},
204204
}
205205

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Tests for #3690: LangGraphAGUIAgent stores Context as Pydantic objects instead of dicts."""
2+
3+
import json
4+
from unittest.mock import MagicMock, patch
5+
from ag_ui.core import Context
6+
7+
8+
class TestAGUIContextSerialization:
9+
"""Verify that context items stored in state are JSON-serializable dicts, not Pydantic objects."""
10+
11+
def test_context_items_are_dicts_not_pydantic(self):
12+
"""Context from ag-ui properties should be model_dump'd before storage."""
13+
from copilotkit.langgraph_agui_agent import LangGraphAGUIAgent
14+
15+
mock_graph = MagicMock()
16+
mock_graph.get_state = MagicMock()
17+
agent = LangGraphAGUIAgent(name="test", graph=mock_graph)
18+
19+
# Pydantic Context objects as they arrive from AG-UI
20+
ctx1 = Context(name="user_info", description="User details", value="John")
21+
ctx2 = Context(name="session", description="Session data", value="abc123")
22+
23+
merged_state = {
24+
'ag-ui': {
25+
'tools': [],
26+
'context': [ctx1, ctx2],
27+
},
28+
'messages': [],
29+
}
30+
31+
with patch.object(
32+
type(agent).__mro__[1], # LangGraphAgent (parent class)
33+
"langgraph_default_merge_state",
34+
return_value=merged_state
35+
):
36+
result = agent.langgraph_default_merge_state({}, [], None)
37+
38+
# Context items must be plain dicts, not Pydantic objects
39+
for item in result['copilotkit']['context']:
40+
assert isinstance(item, dict), f"Expected dict, got {type(item)}"
41+
json.dumps(item) # Must be JSON-serializable
42+
43+
def test_context_with_mixed_types(self):
44+
"""If context contains both Pydantic objects and plain dicts, handle both."""
45+
from copilotkit.langgraph_agui_agent import LangGraphAGUIAgent
46+
47+
mock_graph = MagicMock()
48+
mock_graph.get_state = MagicMock()
49+
agent = LangGraphAGUIAgent(name="test", graph=mock_graph)
50+
51+
ctx_pydantic = Context(name="key1", description="desc", value="val1")
52+
ctx_dict = {"name": "key2", "description": "desc2", "value": "val2"}
53+
54+
merged_state = {
55+
'ag-ui': {
56+
'tools': [],
57+
'context': [ctx_pydantic, ctx_dict],
58+
},
59+
'messages': [],
60+
}
61+
62+
with patch.object(
63+
type(agent).__mro__[1],
64+
"langgraph_default_merge_state",
65+
return_value=merged_state
66+
):
67+
result = agent.langgraph_default_merge_state({}, [], None)
68+
69+
for item in result['copilotkit']['context']:
70+
assert isinstance(item, dict), f"Expected dict, got {type(item)}"
71+
json.dumps(item)

0 commit comments

Comments
 (0)