This commit is contained in:
2026-02-19 17:50:14 +00:00
parent da95a64fb7
commit 6f629c53a6
171 changed files with 7281 additions and 1144 deletions

View File

@@ -0,0 +1,15 @@
"""Controllers module."""
from .conversation import router as conversation_router
from .notification import router as notification_router
from .llm_webhook import router as llm_webhook_router
from .quick_replies import router as quick_replies_router
from .data_purge import router as data_purge_router
__all__ = [
"conversation_router",
"notification_router",
"llm_webhook_router",
"quick_replies_router",
"data_purge_router",
]

View File

@@ -0,0 +1,49 @@
"""
Copyright 2025 Google. This software is provided as-is, without warranty or
representation for any use or purpose. Your use of it is subject to your
agreement with Google.
Conversation API endpoints.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from ..models import ExternalConvRequestDTO, DetectIntentResponseDTO
from ..services import ConversationManagerService
from ..dependencies import get_conversation_manager
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/dialogflow", tags=["conversation"])
@router.post("/detect-intent", response_model=DetectIntentResponseDTO)
async def detect_intent(
request: ExternalConvRequestDTO,
conversation_manager: ConversationManagerService = Depends(
get_conversation_manager
),
) -> DetectIntentResponseDTO:
"""
Detect user intent and manage conversation.
Args:
request: External conversation request from client
Returns:
Dialogflow detect intent response
"""
try:
logger.info("Received detect-intent request")
response = await conversation_manager.manage_conversation(request)
logger.info("Successfully processed detect-intent request")
return response
except ValueError as e:
logger.error(f"Validation error: {str(e)}", exc_info=True)
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error processing detect-intent: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,44 @@
"""
Copyright 2025 Google. This software is provided as-is, without warranty or
representation for any use or purpose. Your use of it is subject to your
agreement with Google.
Data purge API endpoints.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from ..services.data_purge import DataPurgeService
from ..services.redis_service import RedisService
from ..dependencies import get_redis_service, get_settings
from ..config import Settings
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/data-purge", tags=["data-purge"])
@router.delete("/all")
async def purge_all_data(
redis_service: RedisService = Depends(get_redis_service),
settings: Settings = Depends(get_settings),
) -> None:
"""
Purge all data from Redis and Firestore.
WARNING: This is a destructive operation that will delete all conversation
and notification data from both Redis and Firestore.
"""
logger.warning(
"Received request to purge all data. This is a destructive operation."
)
try:
purge_service = DataPurgeService(settings, redis_service)
await purge_service.purge_all_data()
await purge_service.close()
logger.info("Successfully purged all data")
except Exception as e:
logger.error(f"Error purging all data: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,99 @@
"""
Copyright 2025 Google. This software is provided as-is, without warranty or
representation for any use or purpose. Your use of it is subject to your
agreement with Google.
LLM webhook API endpoints for Dialogflow CX integration.
"""
import logging
from fastapi import APIRouter, Depends
from ..models.llm_webhook import WebhookRequestDTO, WebhookResponseDTO, SessionInfoDTO
from ..services.llm_response_tuner import LlmResponseTunerService
from ..dependencies import get_llm_response_tuner
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/llm", tags=["llm-webhook"])
@router.post("/tune-response", response_model=WebhookResponseDTO)
async def tune_response(
request: WebhookRequestDTO,
llm_tuner: LlmResponseTunerService = Depends(get_llm_response_tuner),
) -> WebhookResponseDTO:
"""
Dialogflow CX webhook to retrieve pre-generated LLM responses.
This endpoint is called by Dialogflow when an intent requires an
LLM-generated response. The UUID is passed in session parameters,
and the service retrieves the pre-computed response from Redis.
Flow:
1. Dialogflow sends webhook request with UUID in parameters
2. Service retrieves response from Redis (1-hour TTL)
3. Returns response in session parameters
Args:
request: Webhook request containing session info with UUID
Returns:
Webhook response with response text or error
Raises:
HTTPException: 400 for validation errors, 500 for internal errors
"""
try:
# Extract UUID from session parameters
uuid = request.sessionInfo.parameters.get("uuid")
if not uuid:
logger.error("No UUID provided in webhook request")
return _create_error_response("UUID parameter is required", is_error=True)
# Retrieve response from Redis
response_text = await llm_tuner.get_value(uuid)
if response_text:
# Success - return response
logger.info(f"Successfully retrieved response for UUID: {uuid}")
return WebhookResponseDTO(
sessionInfo=SessionInfoDTO(
parameters={
"webhook_success": True,
"response": response_text,
}
)
)
else:
# Not found
logger.warning(f"No response found for UUID: {uuid}")
return _create_error_response(
"No response found for the given UUID.", is_error=False
)
except Exception as e:
logger.error(f"Error in tune-response webhook: {e}", exc_info=True)
return _create_error_response("An internal error occurred.", is_error=True)
def _create_error_response(error_message: str, is_error: bool) -> WebhookResponseDTO:
"""
Create error response for webhook.
Args:
error_message: Error message to return
is_error: Whether this is a critical error
Returns:
Webhook response with error info
"""
return WebhookResponseDTO(
sessionInfo=SessionInfoDTO(
parameters={
"webhook_success": False,
"error_message": error_message,
}
)
)

View File

@@ -0,0 +1,61 @@
"""
Copyright 2025 Google. This software is provided as-is, without warranty or
representation for any use or purpose. Your use of it is subject to your
agreement with Google.
Notification API endpoints.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from ..models.notification import ExternalNotRequestDTO
from ..services.notification_manager import NotificationManagerService
from ..dependencies import get_notification_manager
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/dialogflow", tags=["notifications"])
@router.post("/notification", status_code=200)
async def process_notification(
request: ExternalNotRequestDTO,
notification_manager: NotificationManagerService = Depends(
get_notification_manager
),
) -> None:
"""
Process push notification from external system.
This endpoint receives notifications (e.g., "Your card was blocked") and:
1. Stores them in Redis/Firestore
2. Associates them with the user's conversation session
3. Triggers a Dialogflow event
When the user later sends a message asking about the notification
("Why was it blocked?"), the message filter will classify it as
NOTIFICATION and route to the appropriate handler.
Args:
request: External notification request with text, phone, and parameters
Returns:
None (204 No Content)
Raises:
HTTPException: 400 if validation fails, 500 for internal errors
"""
try:
logger.info("Received notification request")
await notification_manager.process_notification(request)
logger.info("Successfully processed notification request")
# Match Java behavior: process but don't return response body
except ValueError as e:
logger.error(f"Validation error: {str(e)}", exc_info=True)
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error processing notification: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,112 @@
"""
Copyright 2025 Google. This software is provided as-is, without warranty or
representation for any use or purpose. Your use of it is subject to your
agreement with Google.
Quick Replies API endpoints.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from ..models.quick_replies import QuickReplyScreenRequestDTO
from ..models import DetectIntentResponseDTO
from ..services.quick_reply_content import QuickReplyContentService
from ..services.redis_service import RedisService
from ..services.firestore_service import FirestoreService
from ..utils.session_id import generate_session_id
from ..models.conversation import ConversationSessionDTO, ConversationEntryDTO
from ..dependencies import get_redis_service, get_firestore_service, get_settings
from ..config import Settings
from datetime import datetime
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/quick-replies", tags=["quick-replies"])
@router.post("/screen", response_model=DetectIntentResponseDTO)
async def start_quick_reply_session(
request: QuickReplyScreenRequestDTO,
redis_service: RedisService = Depends(get_redis_service),
firestore_service: FirestoreService = Depends(get_firestore_service),
settings: Settings = Depends(get_settings),
) -> DetectIntentResponseDTO:
"""
Start a quick reply FAQ session for a specific screen.
Creates a conversation session with pantalla_contexto set,
loads the quick reply questions for the screen, and returns them.
Args:
request: Quick reply screen request
Returns:
Detect intent response with quick reply questions
"""
try:
telefono = request.telefono
if not telefono or not telefono.strip():
raise ValueError("Phone number is required")
# Generate session ID
session_id = generate_session_id()
user_id = f"user_by_phone_{telefono.replace(' ', '').replace('-', '')}"
# Create system entry
system_entry = ConversationEntryDTO(
entity="SISTEMA",
type="INICIO",
timestamp=datetime.now(),
text=f"Pantalla: {request.pantalla_contexto} Agregada a la conversacion",
parameters=None,
intent=None,
)
# Create new session with pantalla_contexto
new_session = ConversationSessionDTO(
sessionId=session_id,
userId=user_id,
telefono=telefono,
createdAt=datetime.now(),
lastModified=datetime.now(),
lastMessage=system_entry.text,
pantallaContexto=request.pantalla_contexto,
)
# Save session and entry
await redis_service.save_session(new_session)
logger.info(
f"Created quick reply session {session_id} for screen: {request.pantalla_contexto}"
)
# Load quick replies
content_service = QuickReplyContentService(settings)
quick_reply_dto = await content_service.get_quick_replies(
request.pantalla_contexto
)
if not quick_reply_dto:
raise ValueError(
f"Quick reply screen not found: {request.pantalla_contexto}"
)
# Background save to Firestore
try:
await firestore_service.save_session(new_session)
await firestore_service.save_entry(session_id, system_entry)
except Exception as e:
logger.error(f"Background Firestore save failed: {e}")
return DetectIntentResponseDTO(
responseId=session_id,
queryResult=None,
quick_replies=quick_reply_dto,
)
except ValueError as e:
logger.error(f"Validation error: {e}", exc_info=True)
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error starting quick reply session: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")