diff --git a/src/agents/extensions/memory/sqlalchemy_session.py b/src/agents/extensions/memory/sqlalchemy_session.py index fd2502e24b..a18afe7f47 100644 --- a/src/agents/extensions/memory/sqlalchemy_session.py +++ b/src/agents/extensions/memory/sqlalchemy_session.py @@ -136,6 +136,7 @@ def __init__( sessions_table: str = "agent_sessions", messages_table: str = "agent_messages", session_settings: SessionSettings | None = None, + ensure_ascii: bool = True, ): """Initializes a new SQLAlchemySession. @@ -150,10 +151,13 @@ def __init__( sessions_table (str, optional): Override the default table name for sessions if needed. messages_table (str, optional): Override the default table name for messages if needed. session_settings (SessionSettings | None, optional): Session configuration settings + ensure_ascii (bool, optional): Whether to escape non-ASCII characters when serializing + session items to JSON. Defaults to True to preserve the historical storage format. """ self.session_id = session_id self.session_settings = session_settings or SessionSettings() self._engine = engine + self._ensure_ascii = ensure_ascii self._configure_sqlite_engine(engine) self._init_lock = ( self._get_table_init_lock(engine, sessions_table, messages_table) @@ -245,7 +249,7 @@ def from_url( async def _serialize_item(self, item: TResponseInputItem) -> str: """Serialize an item to JSON string. Can be overridden by subclasses.""" - return json.dumps(item, separators=(",", ":")) + return json.dumps(item, ensure_ascii=self._ensure_ascii, separators=(",", ":")) async def _deserialize_item(self, item: str) -> TResponseInputItem: """Deserialize a JSON string to an item. Can be overridden by subclasses.""" diff --git a/tests/extensions/memory/test_sqlalchemy_session.py b/tests/extensions/memory/test_sqlalchemy_session.py index fe30993699..091f88a482 100644 --- a/tests/extensions/memory/test_sqlalchemy_session.py +++ b/tests/extensions/memory/test_sqlalchemy_session.py @@ -106,6 +106,51 @@ async def test_sqlalchemy_session_direct_ops(agent: Agent): assert len(retrieved_after_clear) == 0 +async def test_sqlalchemy_session_defaults_to_escaped_non_ascii_storage(): + """Default storage keeps the historical escaped non-ASCII JSON representation.""" + session = SQLAlchemySession.from_url("default_ascii_storage", url=DB_URL, create_tables=True) + item: TResponseInputItem = {"role": "user", "content": "café"} + + await session.add_items([item]) + + async with session._session_factory() as sess: + rows = await sess.execute( + select(session._messages.c.message_data).where( + session._messages.c.session_id == session.session_id + ) + ) + stored = rows.scalar_one() + + assert "\\u00e9" in stored + assert "café" not in stored + assert await session.get_items() == [item] + + +async def test_sqlalchemy_session_can_store_non_ascii_without_escaping(): + """ensure_ascii=False stores multilingual content readably while preserving round-trip data.""" + session = SQLAlchemySession.from_url( + "non_ascii_storage", + url=DB_URL, + create_tables=True, + ensure_ascii=False, + ) + item: TResponseInputItem = {"role": "user", "content": "café"} + + await session.add_items([item]) + + async with session._session_factory() as sess: + rows = await sess.execute( + select(session._messages.c.message_data).where( + session._messages.c.session_id == session.session_id + ) + ) + stored = rows.scalar_one() + + assert "café" in stored + assert "\\u00e9" not in stored + assert await session.get_items() == [item] + + async def test_runner_integration(agent: Agent): """Test that SQLAlchemySession works correctly with the agent Runner.""" session_id = "runner_integration_test"