.
This commit is contained in:
324
src/capa_de_integracion/services/firestore_service.py
Normal file
324
src/capa_de_integracion/services/firestore_service.py
Normal 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
|
||||
Reference in New Issue
Block a user