This commit is contained in:
2026-02-19 17:50:14 +00:00
committed by Anibal Angulo
parent b63a1ae4a7
commit 41ba38495b
171 changed files with 7281 additions and 1144 deletions

View File

@@ -0,0 +1,324 @@
"""
Copyright 2025 Google. This software is provided as-is, without warranty or
representation for any use or purpose. Your use of it is subject to your
agreement with Google.
Firestore service for persistent conversation storage.
"""
import logging
from datetime import datetime
from google.cloud import firestore
from ..config import Settings
from ..models import ConversationSessionDTO, ConversationEntryDTO
from ..models.notification import NotificationDTO
logger = logging.getLogger(__name__)
class FirestoreService:
"""Service for Firestore operations on conversations."""
def __init__(self, settings: Settings):
"""Initialize Firestore client."""
self.settings = settings
self.db = firestore.AsyncClient(
project=settings.gcp_project_id,
database=settings.firestore_database_id,
)
self.conversations_collection = (
f"artifacts/{settings.gcp_project_id}/conversations"
)
self.entries_subcollection = "mensajes"
self.notifications_collection = (
f"artifacts/{settings.gcp_project_id}/notifications"
)
logger.info(
f"Firestore client initialized for project: {settings.gcp_project_id}"
)
async def close(self):
"""Close Firestore client."""
await self.db.close()
logger.info("Firestore client closed")
def _session_ref(self, session_id: str):
"""Get Firestore document reference for session."""
return self.db.collection(self.conversations_collection).document(session_id)
async def get_session(self, session_id: str) -> ConversationSessionDTO | None:
"""Retrieve conversation session from Firestore by session ID."""
try:
doc_ref = self._session_ref(session_id)
doc = await doc_ref.get()
if not doc.exists:
logger.debug(f"Session not found in Firestore: {session_id}")
return None
data = doc.to_dict()
session = ConversationSessionDTO.model_validate(data)
logger.debug(f"Retrieved session from Firestore: {session_id}")
return session
except Exception as e:
logger.error(
f"Error retrieving session {session_id} from Firestore: {str(e)}"
)
return None
async def get_session_by_phone(
self, telefono: str
) -> ConversationSessionDTO | None:
"""
Retrieve most recent conversation session from Firestore by phone number.
Args:
telefono: User phone number
Returns:
Most recent session for this phone, or None if not found
"""
try:
query = (
self.db.collection(self.sessions_collection)
.where("telefono", "==", telefono)
.order_by("lastModified", direction=firestore.Query.DESCENDING)
.limit(1)
)
docs = query.stream()
async for doc in docs:
data = doc.to_dict()
session = ConversationSessionDTO.model_validate(data)
logger.debug(
f"Retrieved session from Firestore for phone {telefono}: {session.sessionId}"
)
return session
logger.debug(f"No session found in Firestore for phone: {telefono}")
return None
except Exception as e:
logger.error(
f"Error querying session by phone {telefono} from Firestore: {str(e)}"
)
return None
async def save_session(self, session: ConversationSessionDTO) -> bool:
"""Save conversation session to Firestore."""
try:
doc_ref = self._session_ref(session.sessionId)
data = session.model_dump()
await doc_ref.set(data, merge=True)
logger.debug(f"Saved session to Firestore: {session.sessionId}")
return True
except Exception as e:
logger.error(
f"Error saving session {session.sessionId} to Firestore: {str(e)}"
)
return False
async def save_entry(self, session_id: str, entry: ConversationEntryDTO) -> bool:
"""Save conversation entry to Firestore subcollection."""
try:
doc_ref = self._session_ref(session_id)
entries_ref = doc_ref.collection(self.entries_subcollection)
# Use timestamp as document ID for chronological ordering
entry_id = entry.timestamp.isoformat()
entry_doc = entries_ref.document(entry_id)
data = entry.model_dump()
await entry_doc.set(data)
logger.debug(f"Saved entry to Firestore for session: {session_id}")
return True
except Exception as e:
logger.error(
f"Error saving entry for session {session_id} to Firestore: {str(e)}"
)
return False
async def get_entries(
self, session_id: str, limit: int = 10
) -> list[ConversationEntryDTO]:
"""Retrieve recent conversation entries from Firestore."""
try:
doc_ref = self._session_ref(session_id)
entries_ref = doc_ref.collection(self.entries_subcollection)
# Get entries ordered by timestamp descending
query = entries_ref.order_by(
"timestamp", direction=firestore.Query.DESCENDING
).limit(limit)
docs = query.stream()
entries = []
async for doc in docs:
entry_data = doc.to_dict()
entry = ConversationEntryDTO.model_validate(entry_data)
entries.append(entry)
# Reverse to get chronological order
entries.reverse()
logger.debug(f"Retrieved {len(entries)} entries for session: {session_id}")
return entries
except Exception as e:
logger.error(
f"Error retrieving entries for session {session_id} from Firestore: {str(e)}"
)
return []
async def delete_session(self, session_id: str) -> bool:
"""Delete conversation session and all entries from Firestore."""
try:
doc_ref = self._session_ref(session_id)
# Delete all entries first
entries_ref = doc_ref.collection(self.entries_subcollection)
async for doc in entries_ref.stream():
await doc.reference.delete()
# Delete session document
await doc_ref.delete()
logger.debug(f"Deleted session from Firestore: {session_id}")
return True
except Exception as e:
logger.error(
f"Error deleting session {session_id} from Firestore: {str(e)}"
)
return False
# ====== Notification Methods ======
def _notification_ref(self, notification_id: str):
"""Get Firestore document reference for notification."""
return self.db.collection(self.notifications_collection).document(
notification_id
)
async def save_or_append_notification(self, new_entry: NotificationDTO) -> None:
"""
Save or append notification entry to Firestore.
Args:
new_entry: Notification entry to save
Raises:
ValueError: If phone number is missing
"""
phone_number = new_entry.telefono
if not phone_number or not phone_number.strip():
raise ValueError("Phone number is required to manage notification entries")
# Use phone number as document ID
notification_session_id = phone_number
try:
doc_ref = self._notification_ref(notification_session_id)
doc = await doc_ref.get()
entry_dict = new_entry.model_dump()
if doc.exists:
# Append to existing session
await doc_ref.update(
{
"notificaciones": firestore.ArrayUnion([entry_dict]),
"ultimaActualizacion": datetime.now(),
}
)
logger.info(
f"Successfully appended notification entry to session {notification_session_id} in Firestore"
)
else:
# Create new notification session
new_session_data = {
"sessionId": notification_session_id,
"telefono": phone_number,
"fechaCreacion": datetime.now(),
"ultimaActualizacion": datetime.now(),
"notificaciones": [entry_dict],
}
await doc_ref.set(new_session_data)
logger.info(
f"Successfully created new notification session {notification_session_id} in Firestore"
)
except Exception as e:
logger.error(
f"Error saving notification to Firestore for phone {phone_number}: {str(e)}",
exc_info=True,
)
raise
async def update_notification_status(self, session_id: str, status: str) -> None:
"""
Update the status of all notifications in a session.
Args:
session_id: Notification session ID (phone number)
status: New status value
"""
try:
doc_ref = self._notification_ref(session_id)
doc = await doc_ref.get()
if not doc.exists:
logger.warning(
f"Notification session {session_id} not found in Firestore. Cannot update status"
)
return
session_data = doc.to_dict()
notifications = session_data.get("notificaciones", [])
# Update status for all notifications
updated_notifications = [
{**notif, "status": status} for notif in notifications
]
await doc_ref.update(
{
"notificaciones": updated_notifications,
"ultimaActualizacion": datetime.now(),
}
)
logger.info(
f"Successfully updated notification status to '{status}' for session {session_id} in Firestore"
)
except Exception as e:
logger.error(
f"Error updating notification status in Firestore for session {session_id}: {str(e)}",
exc_info=True,
)
raise
async def delete_notification(self, notification_id: str) -> bool:
"""Delete notification session from Firestore."""
try:
logger.info(
f"Deleting notification session {notification_id} from Firestore"
)
doc_ref = self._notification_ref(notification_id)
await doc_ref.delete()
logger.info(
f"Successfully deleted notification session {notification_id} from Firestore"
)
return True
except Exception as e:
logger.error(
f"Error deleting notification session {notification_id} from Firestore: {str(e)}",
exc_info=True,
)
return False