5 Commits

Author SHA1 Message Date
ac27d12ed3 Add notification model (#31)
All checks were successful
CI / ci (push) Successful in 21s
Co-authored-by: Anibal Angulo <a8065384@banorte.com>
Reviewed-on: #31
2026-03-10 23:50:41 +00:00
a264276a5d Merge pull request 'refactor: timestamp compatible with Firestore' (#30) from refactor/timestamp-to-date into main
Some checks failed
CI / ci (push) Failing after 12s
Reviewed-on: #30
2026-03-10 23:47:48 +00:00
70a3f618bd Merge branch 'main' into refactor/timestamp-to-date
All checks were successful
CI / ci (pull_request) Successful in 20s
2026-03-10 22:56:55 +00:00
f3515ee71c fix(session): use datetime UTC and tighten timestamp logging
All checks were successful
CI / ci (pull_request) Successful in 19s
2026-03-10 21:24:11 +00:00
93c870c8d6 fix(session): normalize firestore timestamps 2026-03-10 21:19:19 +00:00
2 changed files with 37 additions and 22 deletions

View File

@@ -78,9 +78,7 @@ class NotificationDocument(BaseModel):
class NotificationBackend(Protocol):
"""Backend-agnostic interface for notification storage."""
async def get_recent_notifications(
self, phone_number: str
) -> list[Notification]:
async def get_recent_notifications(self, phone_number: str) -> list[Notification]:
"""Return recent notifications for *phone_number*."""
...
@@ -113,9 +111,7 @@ class FirestoreNotificationBackend:
self._max_to_notify = max_to_notify
self._window_hours = window_hours
async def get_recent_notifications(
self, phone_number: str
) -> list[Notification]:
async def get_recent_notifications(self, phone_number: str) -> list[Notification]:
"""Get recent notifications for a user.
Retrieves notifications created within the configured time window,
@@ -148,9 +144,7 @@ class FirestoreNotificationBackend:
cutoff = time.time() - (self._window_hours * 3600)
parsed = [
n
for n in document.notificaciones
if n.timestamp_creacion >= cutoff
n for n in document.notificaciones if n.timestamp_creacion >= cutoff
]
if not parsed:
@@ -212,9 +206,7 @@ class RedisNotificationBackend:
self._max_to_notify = max_to_notify
self._window_hours = window_hours
async def get_recent_notifications(
self, phone_number: str
) -> list[Notification]:
async def get_recent_notifications(self, phone_number: str) -> list[Notification]:
"""Get recent notifications for a user from Redis.
Reads from the ``notification:{phone}`` key, parses the JSON
@@ -246,9 +238,7 @@ class RedisNotificationBackend:
cutoff = time.time() - (self._window_hours * 3600)
parsed = [
n
for n in document.notificaciones
if n.timestamp_creacion >= cutoff
n for n in document.notificaciones if n.timestamp_creacion >= cutoff
]
if not parsed:

View File

@@ -6,6 +6,7 @@ import asyncio
import logging
import time
import uuid
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, override
from google.adk.errors.already_exists_error import AlreadyExistsError
@@ -102,6 +103,24 @@ class FirestoreSessionService(BaseSessionService):
def _events_col(self, app_name: str, user_id: str, session_id: str) -> Any:
return self._session_ref(app_name, user_id, session_id).collection("events")
@staticmethod
def _timestamp_to_float(value: Any, default: float = 0.0) -> float:
if value is None:
return default
if isinstance(value, (int, float)):
return float(value)
if hasattr(value, "timestamp"):
try:
return float(value.timestamp())
except (
TypeError,
ValueError,
OSError,
OverflowError,
) as exc: # pragma: no cover
logger.debug("Failed to convert timestamp %r: %s", value, exc)
return default
# ------------------------------------------------------------------
# State helpers
# ------------------------------------------------------------------
@@ -171,7 +190,7 @@ class FirestoreSessionService(BaseSessionService):
)
)
now = time.time()
now = datetime.now(UTC)
write_coros.append(
self._session_ref(app_name, user_id, session_id).set(
{
@@ -196,7 +215,7 @@ class FirestoreSessionService(BaseSessionService):
user_id=user_id,
id=session_id,
state=merged,
last_update_time=now,
last_update_time=now.timestamp(),
)
@override
@@ -283,7 +302,9 @@ class FirestoreSessionService(BaseSessionService):
id=session_id,
state=merged,
events=events,
last_update_time=session_data.get("last_update_time", 0.0),
last_update_time=self._timestamp_to_float(
session_data.get("last_update_time"), 0.0
),
)
@override
@@ -326,7 +347,9 @@ class FirestoreSessionService(BaseSessionService):
id=data["session_id"],
state=merged,
events=[],
last_update_time=data.get("last_update_time", 0.0),
last_update_time=self._timestamp_to_float(
data.get("last_update_time"), 0.0
),
)
)
@@ -366,6 +389,8 @@ class FirestoreSessionService(BaseSessionService):
# Persist state deltas
session_ref = self._session_ref(app_name, user_id, session_id)
last_update_dt = datetime.fromtimestamp(event.timestamp, UTC)
if event.actions and event.actions.state_delta:
state_deltas = _session_util.extract_state_delta(event.actions.state_delta)
@@ -386,16 +411,16 @@ class FirestoreSessionService(BaseSessionService):
FieldPath("state", k).to_api_repr(): v
for k, v in state_deltas["session"].items()
}
field_updates["last_update_time"] = event.timestamp
field_updates["last_update_time"] = last_update_dt
write_coros.append(session_ref.update(field_updates))
else:
write_coros.append(
session_ref.update({"last_update_time": event.timestamp})
session_ref.update({"last_update_time": last_update_dt})
)
await asyncio.gather(*write_coros)
else:
await session_ref.update({"last_update_time": event.timestamp})
await session_ref.update({"last_update_time": last_update_dt})
# Log token usage
if event.usage_metadata: