Files
int-layer/src/capa_de_integracion/services/firestore_service.py
2026-02-20 19:32:54 +00:00

395 lines
13 KiB
Python

import logging
from datetime import datetime
from google.cloud import firestore
from ..config import Settings
from ..models import ConversationSessionDTO, ConversationEntryDTO
from ..models.notification import Notification
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."""
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.conversations_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 create_session(
self,
session_id: str,
user_id: str,
telefono: str,
pantalla_contexto: str | None = None,
last_message: str | None = None,
) -> ConversationSessionDTO:
"""Create and save a new conversation session to Firestore.
Args:
session_id: Unique session identifier
user_id: User identifier
telefono: User phone number
pantalla_contexto: Optional screen context for the conversation
last_message: Optional last message in the conversation
Returns:
The created session
Raises:
Exception: If session creation or save fails
"""
session = ConversationSessionDTO.create(
session_id=session_id,
user_id=user_id,
telefono=telefono,
pantalla_contexto=pantalla_contexto,
last_message=last_message,
)
doc_ref = self._session_ref(session.sessionId)
data = session.model_dump()
await doc_ref.set(data, merge=True)
logger.info(f"Created new session in Firestore: {session_id}")
return session
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
async def update_pantalla_contexto(
self, session_id: str, pantalla_contexto: str | None
) -> bool:
"""Update the pantallaContexto field for a conversation session.
Args:
session_id: Session ID to update
pantalla_contexto: New pantalla contexto value
Returns:
True if update was successful, False otherwise
"""
try:
doc_ref = self._session_ref(session_id)
doc = await doc_ref.get()
if not doc.exists:
logger.warning(
f"Session {session_id} not found in Firestore. Cannot update pantallaContexto"
)
return False
await doc_ref.update(
{
"pantallaContexto": pantalla_contexto,
"lastModified": datetime.now(),
}
)
logger.debug(
f"Updated pantallaContexto for session {session_id} in Firestore"
)
return True
except Exception as e:
logger.error(
f"Error updating pantallaContexto for session {session_id} in 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: Notification) -> 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