Skip to content
Open
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
694 changes: 547 additions & 147 deletions sqlspec/adapters/adbc/adk/store.py

Large diffs are not rendered by default.

819 changes: 483 additions & 336 deletions sqlspec/adapters/aiomysql/adk/store.py

Large diffs are not rendered by default.

616 changes: 434 additions & 182 deletions sqlspec/adapters/aiosqlite/adk/store.py

Large diffs are not rendered by default.

810 changes: 463 additions & 347 deletions sqlspec/adapters/asyncmy/adk/store.py

Large diffs are not rendered by default.

360 changes: 275 additions & 85 deletions sqlspec/adapters/asyncpg/adk/store.py

Large diffs are not rendered by default.

429 changes: 299 additions & 130 deletions sqlspec/adapters/cockroach_asyncpg/adk/store.py

Large diffs are not rendered by default.

1,098 changes: 744 additions & 354 deletions sqlspec/adapters/cockroach_psycopg/adk/store.py

Large diffs are not rendered by default.

685 changes: 488 additions & 197 deletions sqlspec/adapters/duckdb/adk/store.py

Large diffs are not rendered by default.

1,255 changes: 769 additions & 486 deletions sqlspec/adapters/mysqlconnector/adk/store.py

Large diffs are not rendered by default.

2,492 changes: 1,691 additions & 801 deletions sqlspec/adapters/oracledb/adk/store.py

Large diffs are not rendered by default.

573 changes: 398 additions & 175 deletions sqlspec/adapters/psqlpy/adk/store.py

Large diffs are not rendered by default.

1,123 changes: 792 additions & 331 deletions sqlspec/adapters/psycopg/adk/store.py

Large diffs are not rendered by default.

1,036 changes: 641 additions & 395 deletions sqlspec/adapters/pymysql/adk/store.py

Large diffs are not rendered by default.

596 changes: 475 additions & 121 deletions sqlspec/adapters/spanner/adk/store.py

Large diffs are not rendered by default.

747 changes: 525 additions & 222 deletions sqlspec/adapters/sqlite/adk/store.py

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions sqlspec/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,10 +606,10 @@ class ADKConfig(TypedDict):
"""

session_table: NotRequired[str]
"""Name of the sessions table. Default: 'adk_sessions'"""
"""Name of the sessions table. Default: 'adk_session'"""

events_table: NotRequired[str]
"""Name of the events table. Default: 'adk_events'"""
"""Name of the events table. Default: 'adk_event'"""

memory_table: NotRequired[str]
"""Name of the memory entries table. Default: 'adk_memory_entries'"""
Expand Down
30 changes: 18 additions & 12 deletions sqlspec/extensions/adk/_config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class _ADKSessionStoreConfig(TypedDict):

session_table: str
events_table: str
app_state_table: str
user_state_table: str
metadata_table: str
owner_id_column: NotRequired[str]


Expand All @@ -45,6 +48,7 @@ class _ADKArtifactStoreConfig(TypedDict):
"""Normalized ADK artifact store configuration."""

artifact_table: str
storage_uri: NotRequired[str]


class _ADKConfigSource(Protocol):
Expand All @@ -66,11 +70,12 @@ def _get_adk_session_store_config(config: _ADKConfigSource) -> _ADKSessionStoreC
"""Return normalized session store table settings."""

adk_config = _get_adk_config_from_extension(config)
session_table = adk_config.get("session_table")
events_table = adk_config.get("events_table")
result: _ADKSessionStoreConfig = {
"session_table": str(session_table) if session_table is not None else "adk_sessions",
"events_table": str(events_table) if events_table is not None else "adk_events",
"session_table": str(adk_config.get("session_table") or "adk_session"),
"events_table": str(adk_config.get("events_table") or "adk_event"),
"app_state_table": str(adk_config.get("app_state_table") or "adk_app_state"),
"user_state_table": str(adk_config.get("user_state_table") or "adk_user_state"),
"metadata_table": str(adk_config.get("metadata_table") or "adk_internal_metadata"),
}
owner_id = adk_config.get("owner_id_column")
if owner_id is not None:
Expand All @@ -83,14 +88,11 @@ def _get_adk_memory_store_config(config: _ADKConfigSource) -> _ADKMemoryStoreCon

adk_config = _get_adk_config_from_extension(config)
enable_memory = adk_config.get("enable_memory")
memory_table = adk_config.get("memory_table")
use_fts = adk_config.get("memory_use_fts")
max_results = adk_config.get("memory_max_results")

result: _ADKMemoryStoreConfig = {
"enable_memory": bool(enable_memory) if enable_memory is not None else True,
"memory_table": str(memory_table) if memory_table is not None else "adk_memory_entries",
"use_fts": bool(use_fts) if use_fts is not None else False,
"memory_table": str(adk_config.get("memory_table") or "adk_memory"),
"use_fts": bool(adk_config.get("memory_use_fts", False)),
"max_results": int(max_results) if isinstance(max_results, int) else 20,
}
owner_id = adk_config.get("owner_id_column")
Expand All @@ -103,8 +105,11 @@ def _get_adk_artifact_store_config(config: _ADKConfigSource) -> _ADKArtifactStor
"""Return normalized artifact store settings."""

adk_config = _get_adk_config_from_extension(config)
artifact_table = adk_config.get("artifact_table")
return {"artifact_table": str(artifact_table) if artifact_table is not None else "adk_artifact_versions"}
result: _ADKArtifactStoreConfig = {"artifact_table": str(adk_config.get("artifact_table") or "adk_artifact")}
storage_uri = adk_config.get("artifact_storage_uri")
if storage_uri is not None:
result["storage_uri"] = str(storage_uri)
return result


def _resolve_adk_store_path(config: Any, store_suffix: str) -> str:
Expand Down Expand Up @@ -178,7 +183,8 @@ def _is_adk_memory_migration_enabled(config: Any) -> bool:
include_memory = adk_config.get("include_memory_migration")
if include_memory is not None:
return bool(include_memory)
return bool(adk_config.get("enable_memory", True))
enable_memory = adk_config.get("enable_memory")
return bool(enable_memory) if enable_memory is not None else True


def _validate_adk_store_registration(config: Any) -> None:
Expand Down
12 changes: 7 additions & 5 deletions sqlspec/extensions/adk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ class SessionRecord(TypedDict):
class EventRecord(TypedDict):
"""Database record for an event.

Stores the full ADK Event as a single JSON blob (``event_json``) alongside
a small number of indexed scalar columns used for query filtering.
Stores the full ADK Event as a single JSON blob (``event_data``) alongside
indexed scalar columns used for scoped query filtering.

This design eliminates column drift with upstream ADK: new Event fields are
automatically captured in ``event_json`` without schema changes.
automatically captured in ``event_data`` without schema changes.
"""

id: str
app_name: str
user_id: str
session_id: str
invocation_id: str
author: str
timestamp: datetime
event_json: "dict[str, Any]"
event_data: "dict[str, Any]"
45 changes: 34 additions & 11 deletions sqlspec/extensions/adk/converters.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Conversion functions between ADK models and database records.

Implements full-event JSON storage: the entire Event is serialized via
``Event.model_dump_json(exclude_none=True)`` into a single ``event_json``
``Event.model_dump(exclude_none=True, mode="json")`` into a single ``event_data``
column, with a small set of indexed scalar columns extracted alongside for
query performance. Reconstruction uses ``Event.model_validate_json()``.
query performance. Reconstruction uses ``Event.model_validate()``.

Also provides scoped-state helpers that normalise ADK state prefixes
(``app:``, ``user:``, ``temp:``) so the shared service layer can split,
Expand Down Expand Up @@ -106,42 +106,51 @@ def record_to_session(record: SessionRecord, events: "list[EventRecord]") -> "Se
# ---------------------------------------------------------------------------


def event_to_record(event: "Event", session_id: str) -> EventRecord:
def event_to_record(event: "Event", app_name: str, user_id: str, session_id: str) -> EventRecord:
"""Convert ADK Event to database record using full-event JSON storage.

The entire Event is serialized into ``event_json`` via Pydantic's
``model_dump_json(exclude_none=True)``. A small number of indexed scalar
columns are extracted alongside for query performance.
The entire Event is serialized into ``event_data`` via Pydantic's
``model_dump(exclude_none=True, mode="json")``. Indexed scalar columns are
extracted alongside for scoped filtering.

Args:
event: ADK Event object.
app_name: Name of the parent app.
user_id: ID of the parent user.
session_id: ID of the parent session.

Returns:
EventRecord for database storage.
"""
event_data = _normalize_event_data(event.model_dump(exclude_none=True, mode="json"))
return EventRecord(
id=event.id,
app_name=app_name,
user_id=user_id,
session_id=session_id,
invocation_id=event.invocation_id,
author=event.author,
timestamp=datetime.fromtimestamp(event.timestamp, tz=timezone.utc),
event_json=event.model_dump(exclude_none=True, mode="json"),
event_data=event_data,
)


def record_to_event(record: "EventRecord") -> "Event":
"""Convert database record to ADK Event.

Reconstruction is lossless: the full Event is restored from
``event_json`` via ``Event.model_validate_json()``.
Reconstruction is lossless for valid ADK payloads: the full Event is
restored from ``event_data`` via ``Event.model_validate()``.

Args:
record: Event database record.

Returns:
ADK Event object.
"""
return Event.model_validate(record["event_json"])
event_data = _normalize_event_data(record["event_data"])
event_data.setdefault("id", record["id"])
event_data.setdefault("invocation_id", record["invocation_id"])
event_data.setdefault("timestamp", record["timestamp"].timestamp())
return Event.model_validate(event_data)


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -213,3 +222,17 @@ def merge_scoped_state(
if user_state is not None:
merged.update(user_state)
return merged


def _normalize_event_data(event_data: "dict[str, Any]") -> "dict[str, Any]":
"""Return event data acceptable to ADK 2.2's Event model.

ADK 2.2 guards an assigned ``event.actions = None`` during service writes,
but explicit ``actions: null`` does not validate as a durable Event shape.
SQLSpec therefore omits that key before storing or restoring payloads.
"""

normalized = dict(event_data)
if normalized.get("actions") is None:
normalized.pop("actions", None)
return normalized
Loading
Loading