Misc improvements

This commit is contained in:
2026-02-20 06:59:31 +00:00
parent 734cade8d9
commit e9d80def08
33 changed files with 1844 additions and 420 deletions

View File

@@ -0,0 +1,9 @@
"""Quick reply services."""
from capa_de_integracion.services.quick_reply.content import QuickReplyContentService
from capa_de_integracion.services.quick_reply.session import QuickReplySessionService
__all__ = [
"QuickReplyContentService",
"QuickReplySessionService",
]

View File

@@ -0,0 +1,161 @@
"""Quick reply content service for loading FAQ screens."""
import json
import logging
from pathlib import Path
from capa_de_integracion.config import Settings
from capa_de_integracion.models.quick_replies import (
QuickReplyQuestions,
QuickReplyScreen,
)
logger = logging.getLogger(__name__)
class QuickReplyContentService:
"""Service for loading quick reply screen content from JSON files."""
def __init__(self, settings: Settings) -> None:
"""Initialize quick reply content service.
Args:
settings: Application settings
"""
self.settings = settings
self.quick_replies_path = settings.base_path / "quick_replies"
self._cache: dict[str, QuickReplyScreen] = {}
logger.info(
"QuickReplyContentService initialized with path: %s",
self.quick_replies_path,
)
# Preload all quick reply files into memory
self._preload_cache()
def _validate_file(self, file_path: Path, screen_id: str) -> None:
"""Validate that the quick reply file exists."""
if not file_path.exists():
logger.warning("Quick reply file not found: %s", file_path)
msg = f"Quick reply file not found for screen_id: {screen_id}"
raise ValueError(msg)
def _parse_quick_reply_data(self, data: dict) -> QuickReplyScreen:
"""Parse JSON data into QuickReplyScreen model.
Args:
data: JSON data dictionary
Returns:
Parsed QuickReplyScreen object
"""
preguntas_data = data.get("preguntas", [])
preguntas = [
QuickReplyQuestions(
titulo=q.get("titulo", ""),
descripcion=q.get("descripcion"),
respuesta=q.get("respuesta", ""),
)
for q in preguntas_data
]
return QuickReplyScreen(
header=data.get("header"),
body=data.get("body"),
button=data.get("button"),
header_section=data.get("header_section"),
preguntas=preguntas,
)
def _preload_cache(self) -> None:
"""Preload all quick reply files into memory cache at startup.
This method runs synchronously at initialization to load all
quick reply JSON files. Blocking here is acceptable since it
only happens once at startup.
"""
if not self.quick_replies_path.exists():
logger.warning(
"Quick replies directory not found: %s",
self.quick_replies_path,
)
return
loaded_count = 0
failed_count = 0
for file_path in self.quick_replies_path.glob("*.json"):
screen_id = file_path.stem
try:
# Blocking I/O is OK at startup
content = file_path.read_text(encoding="utf-8")
data = json.loads(content)
quick_reply = self._parse_quick_reply_data(data)
self._cache[screen_id] = quick_reply
loaded_count += 1
logger.debug(
"Cached %s quick replies for screen: %s",
len(quick_reply.preguntas),
screen_id,
)
except json.JSONDecodeError:
logger.exception("Invalid JSON in file: %s", file_path)
failed_count += 1
except Exception:
logger.exception("Failed to load quick reply file: %s", file_path)
failed_count += 1
logger.info(
"Quick reply cache initialized: %s screens loaded, %s failed",
loaded_count,
failed_count,
)
async def get_quick_replies(self, screen_id: str) -> QuickReplyScreen:
"""Get quick reply screen content by ID from in-memory cache.
This method is non-blocking as it retrieves data from the
in-memory cache populated at startup.
Args:
screen_id: Screen identifier (e.g., "pagos", "home")
Returns:
Quick reply screen data
Raises:
ValueError: If the quick reply is not found in cache
"""
if not screen_id or not screen_id.strip():
logger.warning("screen_id is null or empty. Returning empty quick replies")
return QuickReplyScreen(
header="empty",
body=None,
button=None,
header_section=None,
preguntas=[],
)
# Non-blocking: just a dictionary lookup
quick_reply = self._cache.get(screen_id)
if quick_reply is None:
logger.warning("Quick reply not found in cache for screen: %s", screen_id)
msg = f"Quick reply not found for screen_id: {screen_id}"
raise ValueError(msg)
logger.info(
"Retrieved %s quick replies for screen: %s from cache",
len(quick_reply.preguntas),
screen_id,
)
return quick_reply

View File

@@ -0,0 +1,125 @@
"""Quick reply session service for managing FAQ sessions."""
import logging
from uuid import uuid4
from capa_de_integracion.models.quick_replies import QuickReplyScreen
from capa_de_integracion.services.quick_reply.content import QuickReplyContentService
from capa_de_integracion.services.storage.firestore import FirestoreService
from capa_de_integracion.services.storage.redis import RedisService
logger = logging.getLogger(__name__)
class QuickReplySessionResponse:
"""Response from quick reply session service."""
def __init__(self, session_id: str, quick_replies: QuickReplyScreen) -> None:
"""Initialize response.
Args:
session_id: The session ID
quick_replies: The quick reply screen data
"""
self.session_id = session_id
self.quick_replies = quick_replies
class QuickReplySessionService:
"""Service for managing quick reply FAQ sessions."""
def __init__(
self,
redis_service: RedisService,
firestore_service: FirestoreService,
quick_reply_content_service: QuickReplyContentService,
) -> None:
"""Initialize quick reply session service.
Args:
redis_service: Redis service instance
firestore_service: Firestore service instance
quick_reply_content_service: Quick reply content service instance
"""
self.redis_service = redis_service
self.firestore_service = firestore_service
self.quick_reply_content_service = quick_reply_content_service
def _validate_phone(self, phone: str) -> None:
"""Validate phone number.
Args:
phone: Phone number to validate
Raises:
ValueError: If phone is empty or invalid
"""
if not phone or not phone.strip():
msg = "Phone number is required"
raise ValueError(msg)
async def start_quick_reply_session(
self,
telefono: str,
_nombre: str,
pantalla_contexto: str,
) -> QuickReplySessionResponse:
"""Start a quick reply FAQ session for a specific screen.
Creates or updates a conversation session with pantalla_contexto set,
loads the quick reply questions for the screen, and returns them.
Args:
telefono: User's phone number
_nombre: User's name (currently unused but part of API contract)
pantalla_contexto: Screen context identifier
Returns:
Quick reply session response with session ID and quick replies
Raises:
ValueError: If validation fails or data is invalid
Exception: If there's an error creating session or loading content
"""
self._validate_phone(telefono)
# Get or create session
session = await self.firestore_service.get_session_by_phone(telefono)
if session:
session_id = session.session_id
await self.firestore_service.update_pantalla_contexto(
session_id,
pantalla_contexto,
)
session.pantalla_contexto = pantalla_contexto
else:
session_id = str(uuid4())
user_id = f"user_by_phone_{telefono.replace(' ', '').replace('-', '')}"
session = await self.firestore_service.create_session(
session_id,
user_id,
telefono,
pantalla_contexto,
)
# Cache session in Redis
await self.redis_service.save_session(session)
logger.info(
"Created quick reply session %s for screen: %s",
session_id,
pantalla_contexto,
)
# Load quick replies for the screen
quick_replies = await self.quick_reply_content_service.get_quick_replies(
pantalla_contexto,
)
return QuickReplySessionResponse(
session_id=session_id,
quick_replies=quick_replies,
)