.
This commit is contained in:
@@ -38,3 +38,7 @@ dev = [
|
|||||||
"ruff>=0.15.1",
|
"ruff>=0.15.1",
|
||||||
"ty>=0.0.17",
|
"ty>=0.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ['ALL']
|
||||||
|
ignore = []
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
"""
|
"""Copyright 2025 Google. This software is provided as-is,
|
||||||
Copyright 2025 Google. This software is provided as-is,
|
|
||||||
without warranty or representation for any use or purpose.
|
without warranty or representation for any use or purpose.
|
||||||
Your use of it is subject to your agreement with Google.
|
Your use of it is subject to your agreement with Google.
|
||||||
|
|
||||||
Capa de Integración - Conversational AI Orchestrator Service
|
Capa de Integración - Conversational AI Orchestrator Service
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .main import main, app
|
from .main import app, main
|
||||||
|
|
||||||
__all__ = ["main", "app"]
|
__all__ = ["app", "main"]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
@@ -18,11 +19,11 @@ class Settings(BaseSettings):
|
|||||||
# Firestore
|
# Firestore
|
||||||
firestore_database_id: str = Field(..., alias="GCP_FIRESTORE_DATABASE_ID")
|
firestore_database_id: str = Field(..., alias="GCP_FIRESTORE_DATABASE_ID")
|
||||||
firestore_host: str = Field(
|
firestore_host: str = Field(
|
||||||
default="firestore.googleapis.com", alias="GCP_FIRESTORE_HOST"
|
default="firestore.googleapis.com", alias="GCP_FIRESTORE_HOST",
|
||||||
)
|
)
|
||||||
firestore_port: int = Field(default=443, alias="GCP_FIRESTORE_PORT")
|
firestore_port: int = Field(default=443, alias="GCP_FIRESTORE_PORT")
|
||||||
firestore_importer_enabled: bool = Field(
|
firestore_importer_enabled: bool = Field(
|
||||||
default=False, alias="GCP_FIRESTORE_IMPORTER_ENABLE"
|
default=False, alias="GCP_FIRESTORE_IMPORTER_ENABLE",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
@@ -35,10 +36,10 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# Conversation Context
|
# Conversation Context
|
||||||
conversation_context_message_limit: int = Field(
|
conversation_context_message_limit: int = Field(
|
||||||
default=60, alias="CONVERSATION_CONTEXT_MESSAGE_LIMIT"
|
default=60, alias="CONVERSATION_CONTEXT_MESSAGE_LIMIT",
|
||||||
)
|
)
|
||||||
conversation_context_days_limit: int = Field(
|
conversation_context_days_limit: int = Field(
|
||||||
default=30, alias="CONVERSATION_CONTEXT_DAYS_LIMIT"
|
default=30, alias="CONVERSATION_CONTEXT_DAYS_LIMIT",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .services import (
|
from .services import (
|
||||||
ConversationManagerService,
|
ConversationManagerService,
|
||||||
|
DLPService,
|
||||||
NotificationManagerService,
|
NotificationManagerService,
|
||||||
QuickReplyContentService,
|
QuickReplyContentService,
|
||||||
DLPService,
|
|
||||||
)
|
)
|
||||||
from .services.redis_service import RedisService
|
|
||||||
from .services.firestore_service import FirestoreService
|
from .services.firestore_service import FirestoreService
|
||||||
from .services.rag_service import RAGService
|
from .services.rag_service import RAGService
|
||||||
|
from .services.redis_service import RedisService
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
@@ -70,7 +71,6 @@ def get_conversation_manager() -> ConversationManagerService:
|
|||||||
def init_services(settings) -> None:
|
def init_services(settings) -> None:
|
||||||
"""Initialize services (placeholder for compatibility)."""
|
"""Initialize services (placeholder for compatibility)."""
|
||||||
# Services are lazy-loaded via lru_cache, no explicit init needed
|
# Services are lazy-loaded via lru_cache, no explicit init needed
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def startup_services() -> None:
|
async def startup_services() -> None:
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
class FirestorePersistenceException(Exception):
|
class FirestorePersistenceException(Exception):
|
||||||
"""
|
"""Exception raised when Firestore operations fail.
|
||||||
Exception raised when Firestore operations fail.
|
|
||||||
|
|
||||||
This is typically caught and logged without failing the request.
|
This is typically caught and logged without failing the request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message: str, cause: Exception | None = None):
|
def __init__(self, message: str, cause: Exception | None = None) -> None:
|
||||||
"""
|
"""Initialize Firestore persistence exception.
|
||||||
Initialize Firestore persistence exception.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message: Error message
|
message: Error message
|
||||||
cause: Original exception that caused this error
|
cause: Original exception that caused this error
|
||||||
|
|
||||||
"""
|
"""
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.cause = cause
|
self.cause = cause
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
|
from .dependencies import init_services, shutdown_services, startup_services
|
||||||
from .routers import conversation_router, notification_router, quick_replies_router
|
from .routers import conversation_router, notification_router, quick_replies_router
|
||||||
from .dependencies import init_services, startup_services, shutdown_services
|
|
||||||
|
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -64,7 +63,7 @@ async def health_check():
|
|||||||
return {"status": "healthy", "service": "capa-de-integracion"}
|
return {"status": "healthy", "service": "capa-de-integracion"}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
"""Entry point for CLI."""
|
"""Entry point for CLI."""
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
"""Data models module."""
|
"""Data models module."""
|
||||||
|
|
||||||
from .conversation import (
|
from .conversation import (
|
||||||
User,
|
|
||||||
ConversationSession,
|
|
||||||
ConversationEntry,
|
ConversationEntry,
|
||||||
ConversationRequest,
|
ConversationRequest,
|
||||||
|
ConversationSession,
|
||||||
DetectIntentResponse,
|
DetectIntentResponse,
|
||||||
QueryResult,
|
QueryResult,
|
||||||
|
User,
|
||||||
)
|
)
|
||||||
from .notification import (
|
from .notification import (
|
||||||
ExternalNotificationRequest,
|
ExternalNotificationRequest,
|
||||||
NotificationSession,
|
|
||||||
Notification,
|
Notification,
|
||||||
|
NotificationSession,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Conversation
|
|
||||||
"User",
|
|
||||||
"ConversationSession",
|
|
||||||
"ConversationEntry",
|
"ConversationEntry",
|
||||||
"ConversationRequest",
|
"ConversationRequest",
|
||||||
|
"ConversationSession",
|
||||||
"DetectIntentResponse",
|
"DetectIntentResponse",
|
||||||
"QueryResult",
|
|
||||||
# Notification
|
# Notification
|
||||||
"ExternalNotificationRequest",
|
"ExternalNotificationRequest",
|
||||||
"NotificationSession",
|
|
||||||
"Notification",
|
"Notification",
|
||||||
|
"NotificationSession",
|
||||||
|
"QueryResult",
|
||||||
|
# Conversation
|
||||||
|
"User",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class Notification(BaseModel):
|
class Notification(BaseModel):
|
||||||
"""
|
"""Individual notification event record.
|
||||||
Individual notification event record.
|
|
||||||
|
|
||||||
Represents a notification to be stored in Firestore and cached in Redis.
|
Represents a notification to be stored in Firestore and cached in Redis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
idNotificacion: str = Field(
|
idNotificacion: str = Field(
|
||||||
..., alias="idNotificacion", description="Unique notification ID"
|
..., alias="idNotificacion", description="Unique notification ID",
|
||||||
)
|
)
|
||||||
telefono: str = Field(..., alias="telefono", description="User phone number")
|
telefono: str = Field(..., alias="telefono", description="User phone number")
|
||||||
timestampCreacion: datetime = Field(
|
timestampCreacion: datetime = Field(
|
||||||
@@ -36,7 +36,7 @@ class Notification(BaseModel):
|
|||||||
description="Session parameters for Dialogflow",
|
description="Session parameters for Dialogflow",
|
||||||
)
|
)
|
||||||
status: str = Field(
|
status: str = Field(
|
||||||
default="active", alias="status", description="Notification status"
|
default="active", alias="status", description="Notification status",
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = {"populate_by_name": True}
|
model_config = {"populate_by_name": True}
|
||||||
@@ -52,8 +52,7 @@ class Notification(BaseModel):
|
|||||||
parametros: dict[str, Any] | None = None,
|
parametros: dict[str, Any] | None = None,
|
||||||
status: str = "active",
|
status: str = "active",
|
||||||
) -> "Notification":
|
) -> "Notification":
|
||||||
"""
|
"""Create a new Notification with auto-filled timestamp.
|
||||||
Create a new Notification with auto-filled timestamp.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
id_notificacion: Unique notification ID
|
id_notificacion: Unique notification ID
|
||||||
@@ -66,6 +65,7 @@ class Notification(BaseModel):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
New Notification instance with current timestamp
|
New Notification instance with current timestamp
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
idNotificacion=id_notificacion,
|
idNotificacion=id_notificacion,
|
||||||
@@ -109,7 +109,7 @@ class ExternalNotificationRequest(BaseModel):
|
|||||||
texto: str = Field(..., min_length=1)
|
texto: str = Field(..., min_length=1)
|
||||||
telefono: str = Field(..., alias="telefono", description="User phone number")
|
telefono: str = Field(..., alias="telefono", description="User phone number")
|
||||||
parametros_ocultos: dict[str, Any] | None = Field(
|
parametros_ocultos: dict[str, Any] | None = Field(
|
||||||
None, alias="parametrosOcultos", description="Hidden parameters (metadata)"
|
None, alias="parametrosOcultos", description="Hidden parameters (metadata)",
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = {"populate_by_name": True}
|
model_config = {"populate_by_name": True}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from ..models import ConversationRequest, DetectIntentResponse
|
from capa_de_integracion.dependencies import get_conversation_manager
|
||||||
from ..services import ConversationManagerService
|
from capa_de_integracion.models import ConversationRequest, DetectIntentResponse
|
||||||
from ..dependencies import get_conversation_manager
|
from capa_de_integracion.services import ConversationManagerService
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1/dialogflow", tags=["conversation"])
|
router = APIRouter(prefix="/api/v1/dialogflow", tags=["conversation"])
|
||||||
|
|
||||||
|
|
||||||
@router.post("/detect-intent", response_model=DetectIntentResponse)
|
@router.post("/detect-intent")
|
||||||
async def detect_intent(
|
async def detect_intent(
|
||||||
request: ConversationRequest,
|
request: ConversationRequest,
|
||||||
conversation_manager: ConversationManagerService = Depends(
|
conversation_manager: Annotated[ConversationManagerService, Depends(
|
||||||
get_conversation_manager
|
get_conversation_manager,
|
||||||
),
|
)],
|
||||||
) -> DetectIntentResponse:
|
) -> DetectIntentResponse:
|
||||||
"""
|
"""Detect user intent and manage conversation.
|
||||||
Detect user intent and manage conversation.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: External conversation request from client
|
request: External conversation request from client
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dialogflow detect intent response
|
Dialogflow detect intent response
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info("Received detect-intent request")
|
logger.info("Received detect-intent request")
|
||||||
@@ -34,9 +35,9 @@ async def detect_intent(
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Validation error: {str(e)}", exc_info=True)
|
logger.error(f"Validation error: {e!s}", exc_info=True)
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing detect-intent: {str(e)}", exc_info=True)
|
logger.error(f"Error processing detect-intent: {e!s}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from ..models.notification import ExternalNotificationRequest
|
from capa_de_integracion.dependencies import get_notification_manager
|
||||||
from ..services.notification_manager import NotificationManagerService
|
from capa_de_integracion.models.notification import ExternalNotificationRequest
|
||||||
from ..dependencies import get_notification_manager
|
from capa_de_integracion.services.notification_manager import NotificationManagerService
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/api/v1/dialogflow", tags=["notifications"])
|
router = APIRouter(prefix="/api/v1/dialogflow", tags=["notifications"])
|
||||||
@@ -13,12 +14,11 @@ router = APIRouter(prefix="/api/v1/dialogflow", tags=["notifications"])
|
|||||||
@router.post("/notification", status_code=200)
|
@router.post("/notification", status_code=200)
|
||||||
async def process_notification(
|
async def process_notification(
|
||||||
request: ExternalNotificationRequest,
|
request: ExternalNotificationRequest,
|
||||||
notification_manager: NotificationManagerService = Depends(
|
notification_manager: Annotated[NotificationManagerService, Depends(
|
||||||
get_notification_manager
|
get_notification_manager,
|
||||||
),
|
)],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Process push notification from external system.
|
||||||
Process push notification from external system.
|
|
||||||
|
|
||||||
This endpoint receives notifications (e.g., "Your card was blocked") and:
|
This endpoint receives notifications (e.g., "Your card was blocked") and:
|
||||||
1. Stores them in Redis/Firestore
|
1. Stores them in Redis/Firestore
|
||||||
@@ -37,6 +37,7 @@ async def process_notification(
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 400 if validation fails, 500 for internal errors
|
HTTPException: 400 if validation fails, 500 for internal errors
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info("Received notification request")
|
logger.info("Received notification request")
|
||||||
@@ -45,9 +46,9 @@ async def process_notification(
|
|||||||
# Match Java behavior: process but don't return response body
|
# Match Java behavior: process but don't return response body
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Validation error: {str(e)}", exc_info=True)
|
logger.error(f"Validation error: {e!s}", exc_info=True)
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing notification: {str(e)}", exc_info=True)
|
logger.error(f"Error processing notification: {e!s}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import logging
|
import logging
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from typing import Annotated
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from ..models.quick_replies import QuickReplyScreen
|
from capa_de_integracion.dependencies import (
|
||||||
from ..services.quick_reply_content import QuickReplyContentService
|
|
||||||
from ..services.redis_service import RedisService
|
|
||||||
from ..services.firestore_service import FirestoreService
|
|
||||||
from ..dependencies import (
|
|
||||||
get_redis_service,
|
|
||||||
get_firestore_service,
|
get_firestore_service,
|
||||||
get_quick_reply_content_service,
|
get_quick_reply_content_service,
|
||||||
|
get_redis_service,
|
||||||
)
|
)
|
||||||
|
from capa_de_integracion.models.quick_replies import QuickReplyScreen
|
||||||
|
from capa_de_integracion.services.firestore_service import FirestoreService
|
||||||
|
from capa_de_integracion.services.quick_reply_content import QuickReplyContentService
|
||||||
|
from capa_de_integracion.services.redis_service import RedisService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/api/v1/quick-replies", tags=["quick-replies"])
|
router = APIRouter(prefix="/api/v1/quick-replies", tags=["quick-replies"])
|
||||||
@@ -38,14 +39,13 @@ class QuickReplyScreenResponse(BaseModel):
|
|||||||
@router.post("/screen")
|
@router.post("/screen")
|
||||||
async def start_quick_reply_session(
|
async def start_quick_reply_session(
|
||||||
request: QuickReplyScreenRequest,
|
request: QuickReplyScreenRequest,
|
||||||
redis_service: RedisService = Depends(get_redis_service),
|
redis_service: Annotated[RedisService, Depends(get_redis_service)],
|
||||||
firestore_service: FirestoreService = Depends(get_firestore_service),
|
firestore_service: Annotated[FirestoreService, Depends(get_firestore_service)],
|
||||||
quick_reply_content_service: QuickReplyContentService = Depends(
|
quick_reply_content_service: Annotated[QuickReplyContentService, Depends(
|
||||||
get_quick_reply_content_service
|
get_quick_reply_content_service,
|
||||||
),
|
)],
|
||||||
) -> QuickReplyScreenResponse:
|
) -> QuickReplyScreenResponse:
|
||||||
"""
|
"""Start a quick reply FAQ session for a specific screen.
|
||||||
Start a quick reply FAQ session for a specific screen.
|
|
||||||
|
|
||||||
Creates a conversation session with pantalla_contexto set,
|
Creates a conversation session with pantalla_contexto set,
|
||||||
loads the quick reply questions for the screen, and returns them.
|
loads the quick reply questions for the screen, and returns them.
|
||||||
@@ -55,39 +55,41 @@ async def start_quick_reply_session(
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Detect intent response with quick reply questions
|
Detect intent response with quick reply questions
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
telefono = request.usuario.telefono
|
telefono = request.usuario.telefono
|
||||||
pantalla_contexto = request.pantallaContexto
|
pantalla_contexto = request.pantallaContexto
|
||||||
if not telefono or not telefono.strip():
|
if not telefono or not telefono.strip():
|
||||||
raise ValueError("Phone number is required")
|
msg = "Phone number is required"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
session = await firestore_service.get_session_by_phone(telefono)
|
session = await firestore_service.get_session_by_phone(telefono)
|
||||||
if session:
|
if session:
|
||||||
session_id = session.sessionId
|
session_id = session.sessionId
|
||||||
await firestore_service.update_pantalla_contexto(
|
await firestore_service.update_pantalla_contexto(
|
||||||
session_id, pantalla_contexto
|
session_id, pantalla_contexto,
|
||||||
)
|
)
|
||||||
session.pantallaContexto = pantalla_contexto
|
session.pantallaContexto = pantalla_contexto
|
||||||
else:
|
else:
|
||||||
session_id = str(uuid4())
|
session_id = str(uuid4())
|
||||||
user_id = f"user_by_phone_{telefono.replace(' ', '').replace('-', '')}"
|
user_id = f"user_by_phone_{telefono.replace(' ', '').replace('-', '')}"
|
||||||
session = await firestore_service.create_session(
|
session = await firestore_service.create_session(
|
||||||
session_id, user_id, telefono, pantalla_contexto
|
session_id, user_id, telefono, pantalla_contexto,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cache session
|
# Cache session
|
||||||
await redis_service.save_session(session)
|
await redis_service.save_session(session)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Created quick reply session {session_id} for screen: {pantalla_contexto}"
|
f"Created quick reply session {session_id} for screen: {pantalla_contexto}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load quick replies
|
# Load quick replies
|
||||||
quick_replies = await quick_reply_content_service.get_quick_replies(
|
quick_replies = await quick_reply_content_service.get_quick_replies(
|
||||||
pantalla_contexto
|
pantalla_contexto,
|
||||||
)
|
)
|
||||||
return QuickReplyScreenResponse(
|
return QuickReplyScreenResponse(
|
||||||
responseId=session_id, quick_replies=quick_replies
|
responseId=session_id, quick_replies=quick_replies,
|
||||||
)
|
)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""Services module."""
|
"""Services module."""
|
||||||
|
|
||||||
from .conversation_manager import ConversationManagerService
|
from .conversation_manager import ConversationManagerService
|
||||||
from .notification_manager import NotificationManagerService
|
|
||||||
from .dlp_service import DLPService
|
from .dlp_service import DLPService
|
||||||
|
from .notification_manager import NotificationManagerService
|
||||||
from .quick_reply_content import QuickReplyContentService
|
from .quick_reply_content import QuickReplyContentService
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"QuickReplyContentService",
|
|
||||||
"ConversationManagerService",
|
"ConversationManagerService",
|
||||||
"NotificationManagerService",
|
|
||||||
"DLPService",
|
"DLPService",
|
||||||
|
"NotificationManagerService",
|
||||||
|
"QuickReplyContentService",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import logging
|
import logging
|
||||||
from uuid import uuid4
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from ..config import Settings
|
from capa_de_integracion.config import Settings
|
||||||
from ..models import (
|
from capa_de_integracion.models import (
|
||||||
|
ConversationEntry,
|
||||||
ConversationRequest,
|
ConversationRequest,
|
||||||
|
ConversationSession,
|
||||||
DetectIntentResponse,
|
DetectIntentResponse,
|
||||||
QueryResult,
|
QueryResult,
|
||||||
ConversationSession,
|
|
||||||
ConversationEntry,
|
|
||||||
)
|
)
|
||||||
from .redis_service import RedisService
|
|
||||||
from .firestore_service import FirestoreService
|
|
||||||
from .dlp_service import DLPService
|
|
||||||
from .rag_service import RAGService
|
|
||||||
from .quick_reply_content import QuickReplyContentService
|
|
||||||
|
|
||||||
|
from .dlp_service import DLPService
|
||||||
|
from .firestore_service import FirestoreService
|
||||||
|
from .quick_reply_content import QuickReplyContentService
|
||||||
|
from .rag_service import RAGService
|
||||||
|
from .redis_service import RedisService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class ConversationManagerService:
|
|||||||
redis_service: RedisService,
|
redis_service: RedisService,
|
||||||
firestore_service: FirestoreService,
|
firestore_service: FirestoreService,
|
||||||
dlp_service: DLPService,
|
dlp_service: DLPService,
|
||||||
):
|
) -> None:
|
||||||
"""Initialize conversation manager."""
|
"""Initialize conversation manager."""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.rag_service = rag_service
|
self.rag_service = rag_service
|
||||||
@@ -47,16 +47,16 @@ class ConversationManagerService:
|
|||||||
logger.info("ConversationManagerService initialized successfully")
|
logger.info("ConversationManagerService initialized successfully")
|
||||||
|
|
||||||
async def manage_conversation(
|
async def manage_conversation(
|
||||||
self, request: ConversationRequest
|
self, request: ConversationRequest,
|
||||||
) -> DetectIntentResponse:
|
) -> DetectIntentResponse:
|
||||||
"""
|
"""Main entry point for managing conversations.
|
||||||
Main entry point for managing conversations.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: External conversation request from client
|
request: External conversation request from client
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Detect intent response from Dialogflow
|
Detect intent response from Dialogflow
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Step 1: DLP obfuscation
|
# Step 1: DLP obfuscation
|
||||||
@@ -75,7 +75,7 @@ class ConversationManagerService:
|
|||||||
session_id = str(uuid4())
|
session_id = str(uuid4())
|
||||||
user_id = f"user_by_phone_{telefono.replace(' ', '').replace('-', '')}"
|
user_id = f"user_by_phone_{telefono.replace(' ', '').replace('-', '')}"
|
||||||
session = await self.firestore_service.create_session(
|
session = await self.firestore_service.create_session(
|
||||||
session_id, user_id, telefono
|
session_id, user_id, telefono,
|
||||||
)
|
)
|
||||||
await self.redis_service.save_session(session)
|
await self.redis_service.save_session(session)
|
||||||
|
|
||||||
@@ -85,10 +85,10 @@ class ConversationManagerService:
|
|||||||
if self._is_pantalla_context_valid(session.lastModified):
|
if self._is_pantalla_context_valid(session.lastModified):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Detected 'pantallaContexto' in session: {session.pantallaContexto}. "
|
f"Detected 'pantallaContexto' in session: {session.pantallaContexto}. "
|
||||||
f"Delegating to QuickReplies flow."
|
f"Delegating to QuickReplies flow.",
|
||||||
)
|
)
|
||||||
response = await self._manage_quick_reply_conversation(
|
response = await self._manage_quick_reply_conversation(
|
||||||
request, session.pantallaContexto
|
request, session.pantallaContexto,
|
||||||
)
|
)
|
||||||
if response:
|
if response:
|
||||||
# Save user message to Firestore
|
# Save user message to Firestore
|
||||||
@@ -101,7 +101,7 @@ class ConversationManagerService:
|
|||||||
canal=getattr(request, "canal", None),
|
canal=getattr(request, "canal", None),
|
||||||
)
|
)
|
||||||
await self.firestore_service.save_entry(
|
await self.firestore_service.save_entry(
|
||||||
session.sessionId, user_entry
|
session.sessionId, user_entry,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save quick reply response to Firestore
|
# Save quick reply response to Firestore
|
||||||
@@ -119,7 +119,7 @@ class ConversationManagerService:
|
|||||||
canal=getattr(request, "canal", None),
|
canal=getattr(request, "canal", None),
|
||||||
)
|
)
|
||||||
await self.firestore_service.save_entry(
|
await self.firestore_service.save_entry(
|
||||||
session.sessionId, assistant_entry
|
session.sessionId, assistant_entry,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update session with last message and timestamp
|
# Update session with last message and timestamp
|
||||||
@@ -131,14 +131,14 @@ class ConversationManagerService:
|
|||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Detected STALE 'pantallaContexto'. Ignoring and proceeding with normal flow."
|
"Detected STALE 'pantallaContexto'. Ignoring and proceeding with normal flow.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 3: Continue with standard conversation flow
|
# Step 3: Continue with standard conversation flow
|
||||||
nickname = request.usuario.nickname
|
nickname = request.usuario.nickname
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Primary Check (Redis): Looking up session for phone: {telefono}"
|
f"Primary Check (Redis): Looking up session for phone: {telefono}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 3a: Load conversation history from Firestore
|
# Step 3a: Load conversation history from Firestore
|
||||||
@@ -165,7 +165,7 @@ class ConversationManagerService:
|
|||||||
logger.info("Sending query to RAG service")
|
logger.info("Sending query to RAG service")
|
||||||
assistant_response = await self.rag_service.query(messages)
|
assistant_response = await self.rag_service.query(messages)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Received response from RAG service: {assistant_response[:100]}..."
|
f"Received response from RAG service: {assistant_response[:100]}...",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 3e: Save user message to Firestore
|
# Step 3e: Save user message to Firestore
|
||||||
@@ -205,7 +205,7 @@ class ConversationManagerService:
|
|||||||
logger.info(f"Marked {len(notifications)} notifications as processed")
|
logger.info(f"Marked {len(notifications)} notifications as processed")
|
||||||
|
|
||||||
# Step 3i: Return response object
|
# Step 3i: Return response object
|
||||||
response = DetectIntentResponse(
|
return DetectIntentResponse(
|
||||||
responseId=str(uuid4()),
|
responseId=str(uuid4()),
|
||||||
queryResult=QueryResult(
|
queryResult=QueryResult(
|
||||||
responseText=assistant_response,
|
responseText=assistant_response,
|
||||||
@@ -214,10 +214,9 @@ class ConversationManagerService:
|
|||||||
quick_replies=None,
|
quick_replies=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error managing conversation: {str(e)}", exc_info=True)
|
logger.error(f"Error managing conversation: {e!s}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _is_pantalla_context_valid(self, last_modified: datetime) -> bool:
|
def _is_pantalla_context_valid(self, last_modified: datetime) -> bool:
|
||||||
@@ -252,17 +251,16 @@ class ConversationManagerService:
|
|||||||
# If no match, use first question as default or delegate to normal flow
|
# If no match, use first question as default or delegate to normal flow
|
||||||
if not matched_answer:
|
if not matched_answer:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"No matching quick reply found for message: '{request.mensaje}'."
|
f"No matching quick reply found for message: '{request.mensaje}'.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create response with the matched quick reply answer
|
# Create response with the matched quick reply answer
|
||||||
response = DetectIntentResponse(
|
return DetectIntentResponse(
|
||||||
responseId=str(uuid4()),
|
responseId=str(uuid4()),
|
||||||
queryResult=QueryResult(responseText=matched_answer, parameters=None),
|
queryResult=QueryResult(responseText=matched_answer, parameters=None),
|
||||||
quick_replies=quick_reply_screen,
|
quick_replies=quick_reply_screen,
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
async def _get_active_notifications(self, telefono: str) -> list:
|
async def _get_active_notifications(self, telefono: str) -> list:
|
||||||
"""Retrieve active notifications for a user from Redis or Firestore.
|
"""Retrieve active notifications for a user from Redis or Firestore.
|
||||||
@@ -272,20 +270,21 @@ class ConversationManagerService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of active Notification objects
|
List of active Notification objects
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Try Redis first
|
# Try Redis first
|
||||||
notification_session = await self.redis_service.get_notification_session(
|
notification_session = await self.redis_service.get_notification_session(
|
||||||
telefono
|
telefono,
|
||||||
)
|
)
|
||||||
|
|
||||||
# If not in Redis, try Firestore
|
# If not in Redis, try Firestore
|
||||||
if not notification_session:
|
if not notification_session:
|
||||||
# Firestore uses phone as document ID for notifications
|
# Firestore uses phone as document ID for notifications
|
||||||
from ..models.notification import NotificationSession
|
from capa_de_integracion.models.notification import NotificationSession
|
||||||
|
|
||||||
doc_ref = self.firestore_service.db.collection(
|
doc_ref = self.firestore_service.db.collection(
|
||||||
self.firestore_service.notifications_collection
|
self.firestore_service.notifications_collection,
|
||||||
).document(telefono)
|
).document(telefono)
|
||||||
doc = await doc_ref.get()
|
doc = await doc_ref.get()
|
||||||
|
|
||||||
@@ -295,18 +294,17 @@ class ConversationManagerService:
|
|||||||
|
|
||||||
# Filter for active notifications only
|
# Filter for active notifications only
|
||||||
if notification_session and notification_session.notificaciones:
|
if notification_session and notification_session.notificaciones:
|
||||||
active_notifications = [
|
return [
|
||||||
notif
|
notif
|
||||||
for notif in notification_session.notificaciones
|
for notif in notification_session.notificaciones
|
||||||
if notif.status == "active"
|
if notif.status == "active"
|
||||||
]
|
]
|
||||||
return active_notifications
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error retrieving notifications for {telefono}: {str(e)}",
|
f"Error retrieving notifications for {telefono}: {e!s}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
@@ -330,6 +328,7 @@ class ConversationManagerService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of messages in OpenAI format [{"role": "...", "content": "..."}]
|
List of messages in OpenAI format [{"role": "...", "content": "..."}]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
@@ -341,7 +340,7 @@ class ConversationManagerService:
|
|||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": f"Historial de conversación:\n{conversation_context}",
|
"content": f"Historial de conversación:\n{conversation_context}",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add system message with notifications if available
|
# Add system message with notifications if available
|
||||||
@@ -355,7 +354,7 @@ class ConversationManagerService:
|
|||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": f"Notificaciones pendientes para el usuario:\n{notifications_text}",
|
"content": f"Notificaciones pendientes para el usuario:\n{notifications_text}",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add system message with user context
|
# Add system message with user context
|
||||||
@@ -372,11 +371,12 @@ class ConversationManagerService:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
telefono: User phone number
|
telefono: User phone number
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Update status in Firestore
|
# Update status in Firestore
|
||||||
await self.firestore_service.update_notification_status(
|
await self.firestore_service.update_notification_status(
|
||||||
telefono, "processed"
|
telefono, "processed",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update or delete from Redis
|
# Update or delete from Redis
|
||||||
@@ -386,7 +386,7 @@ class ConversationManagerService:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error marking notifications as processed for {telefono}: {str(e)}",
|
f"Error marking notifications as processed for {telefono}: {e!s}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -408,13 +408,14 @@ class ConversationManagerService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted conversation text
|
Formatted conversation text
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not entries:
|
if not entries:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# Filter by date (30 days)
|
# Filter by date (30 days)
|
||||||
cutoff_date = datetime.now() - timedelta(
|
cutoff_date = datetime.now() - timedelta(
|
||||||
days=self.settings.conversation_context_days_limit
|
days=self.settings.conversation_context_days_limit,
|
||||||
)
|
)
|
||||||
recent_entries = [
|
recent_entries = [
|
||||||
e for e in entries if e.timestamp and e.timestamp >= cutoff_date
|
e for e in entries if e.timestamp and e.timestamp >= cutoff_date
|
||||||
@@ -439,6 +440,7 @@ class ConversationManagerService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted text, truncated if necessary
|
Formatted text, truncated if necessary
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not entries:
|
if not entries:
|
||||||
return ""
|
return ""
|
||||||
@@ -470,6 +472,7 @@ class ConversationManagerService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string (e.g., "User: hello", "Assistant: hi there")
|
Formatted string (e.g., "User: hello", "Assistant: hi there")
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Map entity to prefix (fixed bug from Java port!)
|
# Map entity to prefix (fixed bug from Java port!)
|
||||||
prefix = "User: " if entry.entity == "user" else "Assistant: "
|
prefix = "User: " if entry.entity == "user" else "Assistant: "
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""
|
"""Copyright 2025 Google. This software is provided as-is, without warranty or
|
||||||
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
|
representation for any use or purpose. Your use of it is subject to your
|
||||||
agreement with Google.
|
agreement with Google.
|
||||||
|
|
||||||
@@ -8,29 +7,28 @@ Data Loss Prevention service for obfuscating sensitive information.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from google.cloud import dlp_v2
|
from google.cloud import dlp_v2
|
||||||
from google.cloud.dlp_v2 import types
|
from google.cloud.dlp_v2 import types
|
||||||
|
|
||||||
from ..config import Settings
|
from capa_de_integracion.config import Settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DLPService:
|
class DLPService:
|
||||||
"""
|
"""Service for detecting and obfuscating sensitive data using Google Cloud DLP.
|
||||||
Service for detecting and obfuscating sensitive data using Google Cloud DLP.
|
|
||||||
|
|
||||||
Integrates with the DLP API to scan text for PII and other sensitive information,
|
Integrates with the DLP API to scan text for PII and other sensitive information,
|
||||||
then obfuscates findings based on their info type.
|
then obfuscates findings based on their info type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, settings: Settings):
|
def __init__(self, settings: Settings) -> None:
|
||||||
"""
|
"""Initialize DLP service.
|
||||||
Initialize DLP service.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings: Application settings
|
settings: Application settings
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.project_id = settings.gcp_project_id
|
self.project_id = settings.gcp_project_id
|
||||||
@@ -40,8 +38,7 @@ class DLPService:
|
|||||||
logger.info("DLP Service initialized")
|
logger.info("DLP Service initialized")
|
||||||
|
|
||||||
async def get_obfuscated_string(self, text: str, template_id: str) -> str:
|
async def get_obfuscated_string(self, text: str, template_id: str) -> str:
|
||||||
"""
|
"""Inspect text for sensitive data and obfuscate findings.
|
||||||
Inspect text for sensitive data and obfuscate findings.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Text to inspect and obfuscate
|
text: Text to inspect and obfuscate
|
||||||
@@ -52,6 +49,7 @@ class DLPService:
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Exception: If DLP API call fails (returns original text on error)
|
Exception: If DLP API call fails (returns original text on error)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Build content item
|
# Build content item
|
||||||
@@ -63,7 +61,7 @@ class DLPService:
|
|||||||
|
|
||||||
# Build inspect config
|
# Build inspect config
|
||||||
finding_limits = types.InspectConfig.FindingLimits(
|
finding_limits = types.InspectConfig.FindingLimits(
|
||||||
max_findings_per_item=0 # No limit
|
max_findings_per_item=0, # No limit
|
||||||
)
|
)
|
||||||
|
|
||||||
inspect_config = types.InspectConfig(
|
inspect_config = types.InspectConfig(
|
||||||
@@ -91,8 +89,7 @@ class DLPService:
|
|||||||
|
|
||||||
if findings_count > 0:
|
if findings_count > 0:
|
||||||
return self._obfuscate_text(response, text)
|
return self._obfuscate_text(response, text)
|
||||||
else:
|
return text
|
||||||
return text
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -102,8 +99,7 @@ class DLPService:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
def _obfuscate_text(self, response: types.InspectContentResponse, text: str) -> str:
|
def _obfuscate_text(self, response: types.InspectContentResponse, text: str) -> str:
|
||||||
"""
|
"""Obfuscate sensitive findings in text.
|
||||||
Obfuscate sensitive findings in text.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
response: DLP inspect content response with findings
|
response: DLP inspect content response with findings
|
||||||
@@ -111,6 +107,7 @@ class DLPService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Text with sensitive data obfuscated
|
Text with sensitive data obfuscated
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Filter findings by likelihood (> POSSIBLE, which is value 3)
|
# Filter findings by likelihood (> POSSIBLE, which is value 3)
|
||||||
findings = [
|
findings = [
|
||||||
@@ -127,7 +124,7 @@ class DLPService:
|
|||||||
info_type = finding.info_type.name
|
info_type = finding.info_type.name
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"InfoType: {info_type} | Likelihood: {finding.likelihood.value}"
|
f"InfoType: {info_type} | Likelihood: {finding.likelihood.value}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Obfuscate based on info type
|
# Obfuscate based on info type
|
||||||
@@ -136,13 +133,11 @@ class DLPService:
|
|||||||
text = text.replace(quote, replacement)
|
text = text.replace(quote, replacement)
|
||||||
|
|
||||||
# Clean up consecutive DIRECCION tags
|
# Clean up consecutive DIRECCION tags
|
||||||
text = self._clean_direccion(text)
|
return self._clean_direccion(text)
|
||||||
|
|
||||||
return text
|
|
||||||
|
|
||||||
def _get_replacement(self, info_type: str, quote: str) -> str | None:
|
def _get_replacement(self, info_type: str, quote: str) -> str | None:
|
||||||
"""
|
"""Get replacement text for a given info type.
|
||||||
Get replacement text for a given info type.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
info_type: DLP info type name
|
info_type: DLP info type name
|
||||||
@@ -150,6 +145,7 @@ class DLPService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Replacement text or None to skip
|
Replacement text or None to skip
|
||||||
|
|
||||||
"""
|
"""
|
||||||
replacements = {
|
replacements = {
|
||||||
"CREDIT_CARD_NUMBER": f"**** **** **** {self._get_last4(quote)}",
|
"CREDIT_CARD_NUMBER": f"**** **** **** {self._get_last4(quote)}",
|
||||||
@@ -190,7 +186,7 @@ class DLPService:
|
|||||||
pattern = r"\[DIRECCION\](?:(?:,\s*|\s+)\[DIRECCION\])*"
|
pattern = r"\[DIRECCION\](?:(?:,\s*|\s+)\[DIRECCION\])*"
|
||||||
return re.sub(pattern, "[DIRECCION]", text).strip()
|
return re.sub(pattern, "[DIRECCION]", text).strip()
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
"""Close DLP client."""
|
"""Close DLP client."""
|
||||||
await self.dlp_client.transport.close()
|
await self.dlp_client.transport.close()
|
||||||
logger.info("DLP client closed")
|
logger.info("DLP client closed")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from google.cloud import firestore
|
from google.cloud import firestore
|
||||||
|
|
||||||
from ..config import Settings
|
from capa_de_integracion.config import Settings
|
||||||
from ..models import ConversationSession, ConversationEntry
|
from capa_de_integracion.models import ConversationEntry, ConversationSession
|
||||||
from ..models.notification import Notification
|
from capa_de_integracion.models.notification import Notification
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class FirestoreService:
|
class FirestoreService:
|
||||||
"""Service for Firestore operations on conversations."""
|
"""Service for Firestore operations on conversations."""
|
||||||
|
|
||||||
def __init__(self, settings: Settings):
|
def __init__(self, settings: Settings) -> None:
|
||||||
"""Initialize Firestore client."""
|
"""Initialize Firestore client."""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.db = firestore.AsyncClient(
|
self.db = firestore.AsyncClient(
|
||||||
@@ -28,10 +28,10 @@ class FirestoreService:
|
|||||||
f"artifacts/{settings.gcp_project_id}/notifications"
|
f"artifacts/{settings.gcp_project_id}/notifications"
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Firestore client initialized for project: {settings.gcp_project_id}"
|
f"Firestore client initialized for project: {settings.gcp_project_id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
"""Close Firestore client."""
|
"""Close Firestore client."""
|
||||||
self.db.close()
|
self.db.close()
|
||||||
logger.info("Firestore client closed")
|
logger.info("Firestore client closed")
|
||||||
@@ -56,20 +56,20 @@ class FirestoreService:
|
|||||||
return session
|
return session
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error retrieving session {session_id} from Firestore: {str(e)}"
|
f"Error retrieving session {session_id} from Firestore: {e!s}",
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_session_by_phone(self, telefono: str) -> ConversationSession | None:
|
async def get_session_by_phone(self, telefono: str) -> ConversationSession | None:
|
||||||
"""
|
"""Retrieve most recent conversation session from Firestore by phone number.
|
||||||
Retrieve most recent conversation session from Firestore by phone number.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
telefono: User phone number
|
telefono: User phone number
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Most recent session for this phone, or None if not found
|
Most recent session for this phone, or None if not found
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
query = (
|
query = (
|
||||||
@@ -84,7 +84,7 @@ class FirestoreService:
|
|||||||
data = doc.to_dict()
|
data = doc.to_dict()
|
||||||
session = ConversationSession.model_validate(data)
|
session = ConversationSession.model_validate(data)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Retrieved session from Firestore for phone {telefono}: {session.sessionId}"
|
f"Retrieved session from Firestore for phone {telefono}: {session.sessionId}",
|
||||||
)
|
)
|
||||||
return session
|
return session
|
||||||
|
|
||||||
@@ -92,8 +92,8 @@ class FirestoreService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error querying session by phone {telefono} from Firestore: {str(e)}"
|
f"Error querying session by phone {telefono} from Firestore: {e!s}",
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -107,8 +107,8 @@ class FirestoreService:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error saving session {session.sessionId} to Firestore: {str(e)}"
|
f"Error saving session {session.sessionId} to Firestore: {e!s}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -134,6 +134,7 @@ class FirestoreService:
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Exception: If session creation or save fails
|
Exception: If session creation or save fails
|
||||||
|
|
||||||
"""
|
"""
|
||||||
session = ConversationSession.create(
|
session = ConversationSession.create(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
@@ -166,13 +167,13 @@ class FirestoreService:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error saving entry for session {session_id} to Firestore: {str(e)}"
|
f"Error saving entry for session {session_id} to Firestore: {e!s}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_entries(
|
async def get_entries(
|
||||||
self, session_id: str, limit: int = 10
|
self, session_id: str, limit: int = 10,
|
||||||
) -> list[ConversationEntry]:
|
) -> list[ConversationEntry]:
|
||||||
"""Retrieve recent conversation entries from Firestore."""
|
"""Retrieve recent conversation entries from Firestore."""
|
||||||
try:
|
try:
|
||||||
@@ -181,7 +182,7 @@ class FirestoreService:
|
|||||||
|
|
||||||
# Get entries ordered by timestamp descending
|
# Get entries ordered by timestamp descending
|
||||||
query = entries_ref.order_by(
|
query = entries_ref.order_by(
|
||||||
"timestamp", direction=firestore.Query.DESCENDING
|
"timestamp", direction=firestore.Query.DESCENDING,
|
||||||
).limit(limit)
|
).limit(limit)
|
||||||
|
|
||||||
docs = query.stream()
|
docs = query.stream()
|
||||||
@@ -198,8 +199,8 @@ class FirestoreService:
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error retrieving entries for session {session_id} from Firestore: {str(e)}"
|
f"Error retrieving entries for session {session_id} from Firestore: {e!s}",
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -219,13 +220,13 @@ class FirestoreService:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error deleting session {session_id} from Firestore: {str(e)}"
|
f"Error deleting session {session_id} from Firestore: {e!s}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def update_pantalla_contexto(
|
async def update_pantalla_contexto(
|
||||||
self, session_id: str, pantalla_contexto: str | None
|
self, session_id: str, pantalla_contexto: str | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Update the pantallaContexto field for a conversation session.
|
"""Update the pantallaContexto field for a conversation session.
|
||||||
|
|
||||||
@@ -235,6 +236,7 @@ class FirestoreService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if update was successful, False otherwise
|
True if update was successful, False otherwise
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
doc_ref = self._session_ref(session_id)
|
doc_ref = self._session_ref(session_id)
|
||||||
@@ -242,7 +244,7 @@ class FirestoreService:
|
|||||||
|
|
||||||
if not doc.exists:
|
if not doc.exists:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Session {session_id} not found in Firestore. Cannot update pantallaContexto"
|
f"Session {session_id} not found in Firestore. Cannot update pantallaContexto",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -250,17 +252,17 @@ class FirestoreService:
|
|||||||
{
|
{
|
||||||
"pantallaContexto": pantalla_contexto,
|
"pantallaContexto": pantalla_contexto,
|
||||||
"lastModified": datetime.now(),
|
"lastModified": datetime.now(),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Updated pantallaContexto for session {session_id} in Firestore"
|
f"Updated pantallaContexto for session {session_id} in Firestore",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error updating pantallaContexto for session {session_id} in Firestore: {str(e)}"
|
f"Error updating pantallaContexto for session {session_id} in Firestore: {e!s}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -269,22 +271,23 @@ class FirestoreService:
|
|||||||
def _notification_ref(self, notification_id: str):
|
def _notification_ref(self, notification_id: str):
|
||||||
"""Get Firestore document reference for notification."""
|
"""Get Firestore document reference for notification."""
|
||||||
return self.db.collection(self.notifications_collection).document(
|
return self.db.collection(self.notifications_collection).document(
|
||||||
notification_id
|
notification_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def save_or_append_notification(self, new_entry: Notification) -> None:
|
async def save_or_append_notification(self, new_entry: Notification) -> None:
|
||||||
"""
|
"""Save or append notification entry to Firestore.
|
||||||
Save or append notification entry to Firestore.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
new_entry: Notification entry to save
|
new_entry: Notification entry to save
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If phone number is missing
|
ValueError: If phone number is missing
|
||||||
|
|
||||||
"""
|
"""
|
||||||
phone_number = new_entry.telefono
|
phone_number = new_entry.telefono
|
||||||
if not phone_number or not phone_number.strip():
|
if not phone_number or not phone_number.strip():
|
||||||
raise ValueError("Phone number is required to manage notification entries")
|
msg = "Phone number is required to manage notification entries"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Use phone number as document ID
|
# Use phone number as document ID
|
||||||
notification_session_id = phone_number
|
notification_session_id = phone_number
|
||||||
@@ -301,10 +304,10 @@ class FirestoreService:
|
|||||||
{
|
{
|
||||||
"notificaciones": firestore.ArrayUnion([entry_dict]),
|
"notificaciones": firestore.ArrayUnion([entry_dict]),
|
||||||
"ultimaActualizacion": datetime.now(),
|
"ultimaActualizacion": datetime.now(),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Successfully appended notification entry to session {notification_session_id} in Firestore"
|
f"Successfully appended notification entry to session {notification_session_id} in Firestore",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Create new notification session
|
# Create new notification session
|
||||||
@@ -317,23 +320,23 @@ class FirestoreService:
|
|||||||
}
|
}
|
||||||
await doc_ref.set(new_session_data)
|
await doc_ref.set(new_session_data)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Successfully created new notification session {notification_session_id} in Firestore"
|
f"Successfully created new notification session {notification_session_id} in Firestore",
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error saving notification to Firestore for phone {phone_number}: {str(e)}",
|
f"Error saving notification to Firestore for phone {phone_number}: {e!s}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def update_notification_status(self, session_id: str, status: str) -> None:
|
async def update_notification_status(self, session_id: str, status: str) -> None:
|
||||||
"""
|
"""Update the status of all notifications in a session.
|
||||||
Update the status of all notifications in a session.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id: Notification session ID (phone number)
|
session_id: Notification session ID (phone number)
|
||||||
status: New status value
|
status: New status value
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
doc_ref = self._notification_ref(session_id)
|
doc_ref = self._notification_ref(session_id)
|
||||||
@@ -341,7 +344,7 @@ class FirestoreService:
|
|||||||
|
|
||||||
if not doc.exists:
|
if not doc.exists:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Notification session {session_id} not found in Firestore. Cannot update status"
|
f"Notification session {session_id} not found in Firestore. Cannot update status",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -357,16 +360,16 @@ class FirestoreService:
|
|||||||
{
|
{
|
||||||
"notificaciones": updated_notifications,
|
"notificaciones": updated_notifications,
|
||||||
"ultimaActualizacion": datetime.now(),
|
"ultimaActualizacion": datetime.now(),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Successfully updated notification status to '{status}' for session {session_id} in Firestore"
|
f"Successfully updated notification status to '{status}' for session {session_id} in Firestore",
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error updating notification status in Firestore for session {session_id}: {str(e)}",
|
f"Error updating notification status in Firestore for session {session_id}: {e!s}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
@@ -375,18 +378,18 @@ class FirestoreService:
|
|||||||
"""Delete notification session from Firestore."""
|
"""Delete notification session from Firestore."""
|
||||||
try:
|
try:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Deleting notification session {notification_id} from Firestore"
|
f"Deleting notification session {notification_id} from Firestore",
|
||||||
)
|
)
|
||||||
doc_ref = self._notification_ref(notification_id)
|
doc_ref = self._notification_ref(notification_id)
|
||||||
await doc_ref.delete()
|
await doc_ref.delete()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Successfully deleted notification session {notification_id} from Firestore"
|
f"Successfully deleted notification session {notification_id} from Firestore",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error deleting notification session {notification_id} from Firestore: {str(e)}",
|
f"Error deleting notification session {notification_id} from Firestore: {e!s}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from ..config import Settings
|
from capa_de_integracion.config import Settings
|
||||||
from ..models.notification import ExternalNotificationRequest, Notification
|
from capa_de_integracion.models.notification import (
|
||||||
from .redis_service import RedisService
|
ExternalNotificationRequest,
|
||||||
from .firestore_service import FirestoreService
|
Notification,
|
||||||
from .dlp_service import DLPService
|
)
|
||||||
|
|
||||||
|
from .dlp_service import DLPService
|
||||||
|
from .firestore_service import FirestoreService
|
||||||
|
from .redis_service import RedisService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -15,8 +18,7 @@ PREFIX_PO_PARAM = "notification_po_"
|
|||||||
|
|
||||||
|
|
||||||
class NotificationManagerService:
|
class NotificationManagerService:
|
||||||
"""
|
"""Manages notification processing and integration with conversations.
|
||||||
Manages notification processing and integration with conversations.
|
|
||||||
|
|
||||||
Handles push notifications from external systems, stores them in
|
Handles push notifications from external systems, stores them in
|
||||||
Redis/Firestore, and triggers Dialogflow event detection.
|
Redis/Firestore, and triggers Dialogflow event detection.
|
||||||
@@ -28,9 +30,8 @@ class NotificationManagerService:
|
|||||||
redis_service: RedisService,
|
redis_service: RedisService,
|
||||||
firestore_service: FirestoreService,
|
firestore_service: FirestoreService,
|
||||||
dlp_service: DLPService,
|
dlp_service: DLPService,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""Initialize notification manager.
|
||||||
Initialize notification manager.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings: Application settings
|
settings: Application settings
|
||||||
@@ -38,6 +39,7 @@ class NotificationManagerService:
|
|||||||
redis_service: Redis caching service
|
redis_service: Redis caching service
|
||||||
firestore_service: Firestore persistence service
|
firestore_service: Firestore persistence service
|
||||||
dlp_service: Data Loss Prevention service
|
dlp_service: Data Loss Prevention service
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.redis_service = redis_service
|
self.redis_service = redis_service
|
||||||
@@ -49,10 +51,9 @@ class NotificationManagerService:
|
|||||||
logger.info("NotificationManagerService initialized")
|
logger.info("NotificationManagerService initialized")
|
||||||
|
|
||||||
async def process_notification(
|
async def process_notification(
|
||||||
self, external_request: ExternalNotificationRequest
|
self, external_request: ExternalNotificationRequest,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Process a push notification from external system.
|
||||||
Process a push notification from external system.
|
|
||||||
|
|
||||||
Flow:
|
Flow:
|
||||||
1. Validate phone number
|
1. Validate phone number
|
||||||
@@ -71,6 +72,7 @@ class NotificationManagerService:
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If phone number is missing
|
ValueError: If phone number is missing
|
||||||
|
|
||||||
"""
|
"""
|
||||||
telefono = external_request.telefono
|
telefono = external_request.telefono
|
||||||
|
|
||||||
@@ -101,17 +103,17 @@ class NotificationManagerService:
|
|||||||
# Save notification to Redis (with async Firestore write-back)
|
# Save notification to Redis (with async Firestore write-back)
|
||||||
await self.redis_service.save_or_append_notification(new_notification_entry)
|
await self.redis_service.save_or_append_notification(new_notification_entry)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Notification for phone {telefono} cached. Kicking off async Firestore write-back"
|
f"Notification for phone {telefono} cached. Kicking off async Firestore write-back",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fire-and-forget Firestore write (matching Java's .subscribe() behavior)
|
# Fire-and-forget Firestore write (matching Java's .subscribe() behavior)
|
||||||
async def save_notification_to_firestore():
|
async def save_notification_to_firestore() -> None:
|
||||||
try:
|
try:
|
||||||
await self.firestore_service.save_or_append_notification(
|
await self.firestore_service.save_or_append_notification(
|
||||||
new_notification_entry
|
new_notification_entry,
|
||||||
)
|
)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Notification entry persisted to Firestore for phone {telefono}"
|
f"Notification entry persisted to Firestore for phone {telefono}",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..config import Settings
|
from capa_de_integracion.config import Settings
|
||||||
from ..models.quick_replies import QuickReplyScreen, QuickReplyQuestions
|
from capa_de_integracion.models.quick_replies import (
|
||||||
|
QuickReplyQuestions,
|
||||||
|
QuickReplyScreen,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -11,23 +13,22 @@ logger = logging.getLogger(__name__)
|
|||||||
class QuickReplyContentService:
|
class QuickReplyContentService:
|
||||||
"""Service for loading quick reply screen content from JSON files."""
|
"""Service for loading quick reply screen content from JSON files."""
|
||||||
|
|
||||||
def __init__(self, settings: Settings):
|
def __init__(self, settings: Settings) -> None:
|
||||||
"""
|
"""Initialize quick reply content service.
|
||||||
Initialize quick reply content service.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings: Application settings
|
settings: Application settings
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.quick_replies_path = settings.base_path / "quick_replies"
|
self.quick_replies_path = settings.base_path / "quick_replies"
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"QuickReplyContentService initialized with path: {self.quick_replies_path}"
|
f"QuickReplyContentService initialized with path: {self.quick_replies_path}",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_quick_replies(self, screen_id: str) -> QuickReplyScreen:
|
async def get_quick_replies(self, screen_id: str) -> QuickReplyScreen:
|
||||||
"""
|
"""Load quick reply screen content by ID.
|
||||||
Load quick reply screen content by ID.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
screen_id: Screen identifier (e.g., "pagos", "home")
|
screen_id: Screen identifier (e.g., "pagos", "home")
|
||||||
@@ -37,6 +38,7 @@ class QuickReplyContentService:
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If the quick reply file is not found
|
ValueError: If the quick reply file is not found
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not screen_id or not screen_id.strip():
|
if not screen_id or not screen_id.strip():
|
||||||
logger.warning("screen_id is null or empty. Returning empty quick replies")
|
logger.warning("screen_id is null or empty. Returning empty quick replies")
|
||||||
@@ -53,11 +55,12 @@ class QuickReplyContentService:
|
|||||||
try:
|
try:
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
logger.warning(f"Quick reply file not found: {file_path}")
|
logger.warning(f"Quick reply file not found: {file_path}")
|
||||||
|
msg = f"Quick reply file not found for screen_id: {screen_id}"
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Quick reply file not found for screen_id: {screen_id}"
|
msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
with open(file_path, encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
# Parse questions
|
# Parse questions
|
||||||
@@ -80,20 +83,22 @@ class QuickReplyContentService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Successfully loaded {len(preguntas)} quick replies for screen: {screen_id}"
|
f"Successfully loaded {len(preguntas)} quick replies for screen: {screen_id}",
|
||||||
)
|
)
|
||||||
return quick_reply
|
return quick_reply
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"Error parsing JSON file {file_path}: {e}", exc_info=True)
|
logger.error(f"Error parsing JSON file {file_path}: {e}", exc_info=True)
|
||||||
|
msg = f"Invalid JSON format in quick reply file for screen_id: {screen_id}"
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid JSON format in quick reply file for screen_id: {screen_id}"
|
msg,
|
||||||
) from e
|
) from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error loading quick replies for screen {screen_id}: {e}",
|
f"Error loading quick replies for screen {screen_id}: {e}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
msg = f"Error loading quick replies for screen_id: {screen_id}"
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Error loading quick replies for screen_id: {screen_id}"
|
msg,
|
||||||
) from e
|
) from e
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from ..config import Settings
|
from capa_de_integracion.config import Settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,8 +28,7 @@ class RAGResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class RAGService:
|
class RAGService:
|
||||||
"""
|
"""Highly concurrent HTTP client for calling RAG endpoints.
|
||||||
Highly concurrent HTTP client for calling RAG endpoints.
|
|
||||||
|
|
||||||
Uses httpx AsyncClient with connection pooling for optimal performance
|
Uses httpx AsyncClient with connection pooling for optimal performance
|
||||||
when handling multiple concurrent requests.
|
when handling multiple concurrent requests.
|
||||||
@@ -41,15 +40,15 @@ class RAGService:
|
|||||||
max_connections: int = 100,
|
max_connections: int = 100,
|
||||||
max_keepalive_connections: int = 20,
|
max_keepalive_connections: int = 20,
|
||||||
timeout: float = 30.0,
|
timeout: float = 30.0,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""Initialize RAG service with connection pooling.
|
||||||
Initialize RAG service with connection pooling.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings: Application settings
|
settings: Application settings
|
||||||
max_connections: Maximum number of concurrent connections
|
max_connections: Maximum number of concurrent connections
|
||||||
max_keepalive_connections: Maximum number of idle connections to keep alive
|
max_keepalive_connections: Maximum number of idle connections to keep alive
|
||||||
timeout: Request timeout in seconds
|
timeout: Request timeout in seconds
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.rag_endpoint_url = settings.rag_endpoint_url
|
self.rag_endpoint_url = settings.rag_endpoint_url
|
||||||
@@ -70,12 +69,11 @@ class RAGService:
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"RAGService initialized with endpoint: {self.rag_endpoint_url}, "
|
f"RAGService initialized with endpoint: {self.rag_endpoint_url}, "
|
||||||
f"max_connections: {max_connections}, timeout: {timeout}s"
|
f"max_connections: {max_connections}, timeout: {timeout}s",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def query(self, messages: list[dict[str, str]]) -> str:
|
async def query(self, messages: list[dict[str, str]]) -> str:
|
||||||
"""
|
"""Send conversation history to RAG endpoint and get response.
|
||||||
Send conversation history to RAG endpoint and get response.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
messages: OpenAI-style conversation history
|
messages: OpenAI-style conversation history
|
||||||
@@ -87,6 +85,7 @@ class RAGService:
|
|||||||
Raises:
|
Raises:
|
||||||
httpx.HTTPError: If HTTP request fails
|
httpx.HTTPError: If HTTP request fails
|
||||||
ValueError: If response format is invalid
|
ValueError: If response format is invalid
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Validate and construct request
|
# Validate and construct request
|
||||||
@@ -113,20 +112,20 @@ class RAGService:
|
|||||||
return rag_response.response
|
return rag_response.response
|
||||||
|
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"HTTP error calling RAG endpoint: {e.response.status_code} - {e.response.text}"
|
f"HTTP error calling RAG endpoint: {e.response.status_code} - {e.response.text}",
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
logger.error(f"Request error calling RAG endpoint: {str(e)}")
|
logger.exception(f"Request error calling RAG endpoint: {e!s}")
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Unexpected error calling RAG endpoint: {str(e)}", exc_info=True
|
f"Unexpected error calling RAG endpoint: {e!s}", exc_info=True,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
"""Close the HTTP client and release connections."""
|
"""Close the HTTP client and release connections."""
|
||||||
await self._client.aclose()
|
await self._client.aclose()
|
||||||
logger.info("RAGService client closed")
|
logger.info("RAGService client closed")
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
from ..config import Settings
|
from capa_de_integracion.config import Settings
|
||||||
from ..models import ConversationSession
|
from capa_de_integracion.models import ConversationSession
|
||||||
from ..models.notification import NotificationSession, Notification
|
from capa_de_integracion.models.notification import Notification, NotificationSession
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class RedisService:
|
class RedisService:
|
||||||
"""Service for Redis operations on conversation sessions."""
|
"""Service for Redis operations on conversation sessions."""
|
||||||
|
|
||||||
def __init__(self, settings: Settings):
|
def __init__(self, settings: Settings) -> None:
|
||||||
"""Initialize Redis client."""
|
"""Initialize Redis client."""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.redis: Redis | None = None
|
self.redis: Redis | None = None
|
||||||
@@ -22,7 +22,7 @@ class RedisService:
|
|||||||
self.notification_ttl = 2592000 # 30 days in seconds
|
self.notification_ttl = 2592000 # 30 days in seconds
|
||||||
self.qr_session_ttl = 86400 # 24 hours in seconds
|
self.qr_session_ttl = 86400 # 24 hours in seconds
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self) -> None:
|
||||||
"""Connect to Redis."""
|
"""Connect to Redis."""
|
||||||
self.redis = Redis(
|
self.redis = Redis(
|
||||||
host=self.settings.redis_host,
|
host=self.settings.redis_host,
|
||||||
@@ -31,10 +31,10 @@ class RedisService:
|
|||||||
decode_responses=True,
|
decode_responses=True,
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Connected to Redis at {self.settings.redis_host}:{self.settings.redis_port}"
|
f"Connected to Redis at {self.settings.redis_host}:{self.settings.redis_port}",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
"""Close Redis connection."""
|
"""Close Redis connection."""
|
||||||
if self.redis:
|
if self.redis:
|
||||||
await self.redis.close()
|
await self.redis.close()
|
||||||
@@ -49,17 +49,18 @@ class RedisService:
|
|||||||
return f"conversation:phone:{phone}"
|
return f"conversation:phone:{phone}"
|
||||||
|
|
||||||
async def get_session(self, session_id_or_phone: str) -> ConversationSession | None:
|
async def get_session(self, session_id_or_phone: str) -> ConversationSession | None:
|
||||||
"""
|
"""Retrieve conversation session from Redis by session ID or phone number.
|
||||||
Retrieve conversation session from Redis by session ID or phone number.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id_or_phone: Either a session ID or phone number
|
session_id_or_phone: Either a session ID or phone number
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Conversation session or None if not found
|
Conversation session or None if not found
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
# First try as phone number (lookup session ID)
|
# First try as phone number (lookup session ID)
|
||||||
phone_key = self._phone_to_session_key(session_id_or_phone)
|
phone_key = self._phone_to_session_key(session_id_or_phone)
|
||||||
@@ -86,17 +87,17 @@ class RedisService:
|
|||||||
logger.debug(f"Retrieved session from Redis: {session_id}")
|
logger.debug(f"Retrieved session from Redis: {session_id}")
|
||||||
return session
|
return session
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deserializing session {session_id}: {str(e)}")
|
logger.exception(f"Error deserializing session {session_id}: {e!s}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def save_session(self, session: ConversationSession) -> bool:
|
async def save_session(self, session: ConversationSession) -> bool:
|
||||||
"""
|
"""Save conversation session to Redis with TTL.
|
||||||
Save conversation session to Redis with TTL.
|
|
||||||
|
|
||||||
Also stores phone-to-session mapping for lookup by phone number.
|
Also stores phone-to-session mapping for lookup by phone number.
|
||||||
"""
|
"""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
key = self._session_key(session.sessionId)
|
key = self._session_key(session.sessionId)
|
||||||
phone_key = self._phone_to_session_key(session.telefono)
|
phone_key = self._phone_to_session_key(session.telefono)
|
||||||
@@ -110,17 +111,18 @@ class RedisService:
|
|||||||
await self.redis.setex(phone_key, self.session_ttl, session.sessionId)
|
await self.redis.setex(phone_key, self.session_ttl, session.sessionId)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Saved session to Redis: {session.sessionId} for phone: {session.telefono}"
|
f"Saved session to Redis: {session.sessionId} for phone: {session.telefono}",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving session {session.sessionId} to Redis: {str(e)}")
|
logger.exception(f"Error saving session {session.sessionId} to Redis: {e!s}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def delete_session(self, session_id: str) -> bool:
|
async def delete_session(self, session_id: str) -> bool:
|
||||||
"""Delete conversation session from Redis."""
|
"""Delete conversation session from Redis."""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
key = self._session_key(session_id)
|
key = self._session_key(session_id)
|
||||||
|
|
||||||
@@ -129,13 +131,14 @@ class RedisService:
|
|||||||
logger.debug(f"Deleted session from Redis: {session_id}")
|
logger.debug(f"Deleted session from Redis: {session_id}")
|
||||||
return result > 0
|
return result > 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting session {session_id} from Redis: {str(e)}")
|
logger.exception(f"Error deleting session {session_id} from Redis: {e!s}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def exists(self, session_id: str) -> bool:
|
async def exists(self, session_id: str) -> bool:
|
||||||
"""Check if session exists in Redis."""
|
"""Check if session exists in Redis."""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
key = self._session_key(session_id)
|
key = self._session_key(session_id)
|
||||||
return await self.redis.exists(key) > 0
|
return await self.redis.exists(key) > 0
|
||||||
@@ -147,8 +150,7 @@ class RedisService:
|
|||||||
return f"conversation:messages:{session_id}"
|
return f"conversation:messages:{session_id}"
|
||||||
|
|
||||||
async def save_message(self, session_id: str, message) -> bool:
|
async def save_message(self, session_id: str, message) -> bool:
|
||||||
"""
|
"""Save a conversation message to Redis sorted set.
|
||||||
Save a conversation message to Redis sorted set.
|
|
||||||
|
|
||||||
Messages are stored in a sorted set with timestamp as score.
|
Messages are stored in a sorted set with timestamp as score.
|
||||||
|
|
||||||
@@ -158,9 +160,11 @@ class RedisService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if successful, False otherwise
|
True if successful, False otherwise
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
key = self._messages_key(session_id)
|
key = self._messages_key(session_id)
|
||||||
|
|
||||||
@@ -178,14 +182,13 @@ class RedisService:
|
|||||||
logger.debug(f"Saved message to Redis: {session_id}")
|
logger.debug(f"Saved message to Redis: {session_id}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error saving message to Redis for session {session_id}: {str(e)}"
|
f"Error saving message to Redis for session {session_id}: {e!s}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_messages(self, session_id: str) -> list:
|
async def get_messages(self, session_id: str) -> list:
|
||||||
"""
|
"""Retrieve all conversation messages for a session from Redis.
|
||||||
Retrieve all conversation messages for a session from Redis.
|
|
||||||
|
|
||||||
Returns messages ordered by timestamp (oldest first).
|
Returns messages ordered by timestamp (oldest first).
|
||||||
|
|
||||||
@@ -194,9 +197,11 @@ class RedisService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of message dictionaries (parsed from JSON)
|
List of message dictionaries (parsed from JSON)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
key = self._messages_key(session_id)
|
key = self._messages_key(session_id)
|
||||||
|
|
||||||
@@ -214,16 +219,16 @@ class RedisService:
|
|||||||
try:
|
try:
|
||||||
messages.append(json.loads(msg_str))
|
messages.append(json.loads(msg_str))
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"Error parsing message JSON: {str(e)}")
|
logger.exception(f"Error parsing message JSON: {e!s}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Retrieved {len(messages)} messages from Redis for session: {session_id}"
|
f"Retrieved {len(messages)} messages from Redis for session: {session_id}",
|
||||||
)
|
)
|
||||||
return messages
|
return messages
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error retrieving messages from Redis for session {session_id}: {str(e)}"
|
f"Error retrieving messages from Redis for session {session_id}: {e!s}",
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -238,21 +243,23 @@ class RedisService:
|
|||||||
return f"notification:phone_to_notification:{phone}"
|
return f"notification:phone_to_notification:{phone}"
|
||||||
|
|
||||||
async def save_or_append_notification(self, new_entry: Notification) -> None:
|
async def save_or_append_notification(self, new_entry: Notification) -> None:
|
||||||
"""
|
"""Save or append notification entry to session.
|
||||||
Save or append notification entry to session.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
new_entry: Notification entry to save
|
new_entry: Notification entry to save
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If phone number is missing
|
ValueError: If phone number is missing
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
phone_number = new_entry.telefono
|
phone_number = new_entry.telefono
|
||||||
if not phone_number or not phone_number.strip():
|
if not phone_number or not phone_number.strip():
|
||||||
raise ValueError("Phone number is required to manage notification entries")
|
msg = "Phone number is required to manage notification entries"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Use phone number as session ID for notifications
|
# Use phone number as session ID for notifications
|
||||||
notification_session_id = phone_number
|
notification_session_id = phone_number
|
||||||
@@ -262,7 +269,7 @@ class RedisService:
|
|||||||
|
|
||||||
if existing_session:
|
if existing_session:
|
||||||
# Append to existing session
|
# Append to existing session
|
||||||
updated_notifications = existing_session.notificaciones + [new_entry]
|
updated_notifications = [*existing_session.notificaciones, new_entry]
|
||||||
updated_session = NotificationSession(
|
updated_session = NotificationSession(
|
||||||
sessionId=notification_session_id,
|
sessionId=notification_session_id,
|
||||||
telefono=phone_number,
|
telefono=phone_number,
|
||||||
@@ -286,7 +293,8 @@ class RedisService:
|
|||||||
async def _cache_notification_session(self, session: NotificationSession) -> bool:
|
async def _cache_notification_session(self, session: NotificationSession) -> bool:
|
||||||
"""Cache notification session in Redis."""
|
"""Cache notification session in Redis."""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
key = self._notification_key(session.sessionId)
|
key = self._notification_key(session.sessionId)
|
||||||
phone_key = self._phone_to_notification_key(session.telefono)
|
phone_key = self._phone_to_notification_key(session.telefono)
|
||||||
@@ -302,17 +310,18 @@ class RedisService:
|
|||||||
logger.debug(f"Cached notification session: {session.sessionId}")
|
logger.debug(f"Cached notification session: {session.sessionId}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error caching notification session {session.sessionId}: {str(e)}"
|
f"Error caching notification session {session.sessionId}: {e!s}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_notification_session(
|
async def get_notification_session(
|
||||||
self, session_id: str
|
self, session_id: str,
|
||||||
) -> NotificationSession | None:
|
) -> NotificationSession | None:
|
||||||
"""Retrieve notification session from Redis."""
|
"""Retrieve notification session from Redis."""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
key = self._notification_key(session_id)
|
key = self._notification_key(session_id)
|
||||||
data = await self.redis.get(key)
|
data = await self.redis.get(key)
|
||||||
@@ -327,15 +336,16 @@ class RedisService:
|
|||||||
logger.info(f"Notification session {session_id} retrieved from Redis")
|
logger.info(f"Notification session {session_id} retrieved from Redis")
|
||||||
return session
|
return session
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error deserializing notification session {session_id}: {str(e)}"
|
f"Error deserializing notification session {session_id}: {e!s}",
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_notification_id_for_phone(self, phone: str) -> str | None:
|
async def get_notification_id_for_phone(self, phone: str) -> str | None:
|
||||||
"""Get notification session ID for a phone number."""
|
"""Get notification session ID for a phone number."""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
key = self._phone_to_notification_key(phone)
|
key = self._phone_to_notification_key(phone)
|
||||||
session_id = await self.redis.get(key)
|
session_id = await self.redis.get(key)
|
||||||
@@ -350,7 +360,8 @@ class RedisService:
|
|||||||
async def delete_notification_session(self, phone_number: str) -> bool:
|
async def delete_notification_session(self, phone_number: str) -> bool:
|
||||||
"""Delete notification session from Redis."""
|
"""Delete notification session from Redis."""
|
||||||
if not self.redis:
|
if not self.redis:
|
||||||
raise RuntimeError("Redis client not connected")
|
msg = "Redis client not connected"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
notification_key = self._notification_key(phone_number)
|
notification_key = self._notification_key(phone_number)
|
||||||
phone_key = self._phone_to_notification_key(phone_number)
|
phone_key = self._phone_to_notification_key(phone_number)
|
||||||
@@ -361,7 +372,7 @@ class RedisService:
|
|||||||
await self.redis.delete(phone_key)
|
await self.redis.delete(phone_key)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
f"Error deleting notification session for phone {phone_number}: {str(e)}"
|
f"Error deleting notification session for phone {phone_number}: {e!s}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user