Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/agents/extensions/memory/sqlalchemy_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)
Expand Down Expand Up @@ -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."""
Expand Down
45 changes: 45 additions & 0 deletions tests/extensions/memory/test_sqlalchemy_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down