From 82db3831995ec3bf1897e5f79a97826b8cf03cb7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 21:36:27 -0700 Subject: [PATCH] Don't try to serialize exceptions - Store their string repr instead --- .../langgraph/checkpoint/serde/jsonplus.py | 5 ++- libs/langgraph/tests/any_str.py | 18 -------- libs/langgraph/tests/memory_assert.py | 2 - libs/langgraph/tests/test_pregel.py | 10 ++--- libs/langgraph/tests/test_pregel_async.py | 42 +++++++++---------- 5 files changed, 30 insertions(+), 47 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index 5465b2802..608a077cd 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -7,6 +7,7 @@ import re from collections import deque from datetime import date, datetime, time, timedelta, timezone from enum import Enum +from inspect import isclass from ipaddress import ( IPv4Address, IPv4Interface, @@ -111,7 +112,7 @@ class JsonPlusSerializer(SerializerProtocol): obj.__class__, method="fromhex", args=[obj.hex()] ) elif isinstance(obj, BaseException): - return self._encode_constructor_args(obj.__class__, args=obj.args) + return repr(obj) else: raise TypeError( f"Object of type {obj.__class__.__name__} is not JSON serializable" @@ -135,6 +136,8 @@ class JsonPlusSerializer(SerializerProtocol): method = getattr(cls, value["method"]) else: method = cls + if isclass(method) and issubclass(method, BaseException): + return None if value["args"] and value["kwargs"]: return method(*value["args"], **value["kwargs"]) elif value["args"]: diff --git a/libs/langgraph/tests/any_str.py b/libs/langgraph/tests/any_str.py index a98962cdc..836cf9371 100644 --- a/libs/langgraph/tests/any_str.py +++ b/libs/langgraph/tests/any_str.py @@ -23,24 +23,6 @@ class AnyVersion: return hash(str(self)) -class ExceptionLike: - def __init__(self, exc: Exception) -> None: - self.exc = exc - - def __eq__(self, value: object) -> bool: - return ( - isinstance(value, Exception) - and self.exc.__class__ == value.__class__ - and str(self.exc) == str(value) - ) - - def __hash__(self) -> int: - return hash((self.exc.__class__, str(self.exc))) - - def __repr__(self) -> str: - return str(self.exc) - - class UnsortedSequence: def __init__(self, *values: Any) -> None: self.seq = values diff --git a/libs/langgraph/tests/memory_assert.py b/libs/langgraph/tests/memory_assert.py index 0b0bcf62f..624a711c7 100644 --- a/libs/langgraph/tests/memory_assert.py +++ b/libs/langgraph/tests/memory_assert.py @@ -24,8 +24,6 @@ class NoopSerializer(SerializerProtocol): class MemorySaverAssertImmutable(MemorySaver): - serde = NoopSerializer() - storage_for_copies: defaultdict[str, dict[str, dict[str, Checkpoint]]] def __init__( diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 06a747a17..7325aa41d 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -64,7 +64,7 @@ from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore -from tests.any_str import AnyStr, AnyVersion, ExceptionLike, UnsortedSequence +from tests.any_str import AnyStr, AnyVersion, UnsortedSequence from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -1307,7 +1307,7 @@ def test_invoke_checkpoint_two( assert checkpoint_tup is not None assert checkpoint_tup.checkpoint["channel_values"].get("total") == 7 assert checkpoint_tup.pending_writes == [ - (AnyStr(), ERROR, ExceptionLike(ValueError("Input is too large"))) + (AnyStr(), ERROR, "ValueError('Input is too large')") ] # on a new thread, total starts out as 0, so output is 0+5=5 assert app.invoke(5, {"configurable": {"thread_id": "2"}}) == 5 @@ -1374,7 +1374,7 @@ def test_pending_writes_resume( assert state.next == ("one", "two") assert state.tasks == ( PregelTask(AnyStr(), "one"), - PregelTask(AnyStr(), "two", ExceptionLike(ConnectionError("I'm not good"))), + PregelTask(AnyStr(), "two", 'ConnectionError("I\'m not good")'), ) assert state.metadata == {"source": "loop", "step": 0, "writes": None} # should contain pending write of "one" @@ -1384,7 +1384,7 @@ def test_pending_writes_resume( expected_writes = [ (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), ERROR, ExceptionLike(ConnectionError("I'm not good"))), + (AnyStr(), ERROR, 'ConnectionError("I\'m not good")'), ] assert len(checkpoint.pending_writes) == 3 assert all(w in expected_writes for w in checkpoint.pending_writes) @@ -1518,7 +1518,7 @@ def test_pending_writes_resume( pending_writes=UnsortedSequence( (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), "__error__", ExceptionLike(ConnectionError("I'm not good"))), + (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), (AnyStr(), "two", "two"), (AnyStr(), "value", 3), ), diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 4eed26017..a35bf7b00 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -61,7 +61,7 @@ from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore -from tests.any_str import AnyStr, AnyVersion, ExceptionLike, UnsortedSequence +from tests.any_str import AnyStr, AnyVersion, UnsortedSequence from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -1593,7 +1593,7 @@ async def test_pending_writes_resume( assert state.next == ("one", "two") assert state.tasks == ( PregelTask(AnyStr(), "one"), - PregelTask(AnyStr(), "two", ExceptionLike(ValueError("I'm not good"))), + PregelTask(AnyStr(), "two", 'ValueError("I\'m not good")'), ) assert state.metadata == {"source": "loop", "step": 0, "writes": None} # should contain pending write of "one" @@ -1603,7 +1603,7 @@ async def test_pending_writes_resume( expected_writes = [ (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), ERROR, ExceptionLike(ValueError("I'm not good"))), + (AnyStr(), ERROR, 'ValueError("I\'m not good")'), ] assert len(checkpoint.pending_writes) == 3 assert all(w in expected_writes for w in checkpoint.pending_writes) @@ -1741,7 +1741,7 @@ async def test_pending_writes_resume( pending_writes=UnsortedSequence( (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), "__error__", ExceptionLike(ValueError("I'm not good"))), + (AnyStr(), "__error__", 'ValueError("I\'m not good")'), (AnyStr(), "two", "two"), (AnyStr(), "value", 3), ), @@ -3108,7 +3108,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: observation = {t.name: t for t in tools}[agent_action.tool].invoke( agent_action.tool_input ) - return {"intermediate_steps": [(agent_action, observation)]} + return {"intermediate_steps": [[agent_action, observation]]} # Define decision-making logic def should_continue(data: AgentState) -> str: @@ -3140,22 +3140,22 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: assert await app.ainvoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -3176,14 +3176,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3199,14 +3199,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -3361,14 +3361,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -3401,14 +3401,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: log="finish:a really nice answer", ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], }, tasks=(), @@ -3537,14 +3537,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -3576,14 +3576,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: log="finish:a really nice answer", ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], }, tasks=(),