""" 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. Dialogflow CX client service for intent detection. """ import logging from google.cloud.dialogflowcx_v3 import SessionsAsyncClient from google.cloud.dialogflowcx_v3.types import ( DetectIntentRequest, QueryInput, TextInput, EventInput, QueryParameters, ) from google.api_core.exceptions import ( GoogleAPIError, InternalServerError, ServiceUnavailable, ) from tenacity import ( retry, stop_after_attempt, wait_exponential, retry_if_exception_type, ) from ..config import Settings from ..models import DetectIntentRequestDTO, DetectIntentResponseDTO, QueryResultDTO logger = logging.getLogger(__name__) class DialogflowClientService: """Service for interacting with Dialogflow CX API.""" def __init__(self, settings: Settings): """Initialize Dialogflow client.""" self.settings = settings self.project_id = settings.dialogflow_project_id self.location = settings.dialogflow_location self.agent_id = settings.dialogflow_agent_id self.default_language = settings.dialogflow_default_language # Initialize async client endpoint = settings.dialogflow_endpoint client_options = {"api_endpoint": endpoint} self.client = SessionsAsyncClient(client_options=client_options) logger.info( f"Dialogflow CX SessionsClient initialized for endpoint: {endpoint}" ) logger.info(f"Agent ID: {self.agent_id}") def _build_session_path(self, session_id: str) -> str: """Build Dialogflow session path.""" return self.client.session_path( project=self.project_id, location=self.location, agent=self.agent_id, session=session_id, ) def _map_query_input(self, query_input_dto) -> QueryInput: """Map QueryInputDTO to Dialogflow QueryInput.""" language_code = query_input_dto.language_code or self.default_language if query_input_dto.text and query_input_dto.text.text: return QueryInput( text=TextInput(text=query_input_dto.text.text), language_code=language_code, ) elif query_input_dto.event and query_input_dto.event.event: return QueryInput( event=EventInput(event=query_input_dto.event.event), language_code=language_code, ) else: raise ValueError("Either text or event input must be provided") def _map_query_params(self, query_params_dto) -> QueryParameters | None: """Map QueryParamsDTO to Dialogflow QueryParameters.""" if not query_params_dto or not query_params_dto.parameters: return None return QueryParameters(parameters=query_params_dto.parameters) def _extract_response_text(self, response) -> str: """Extract text from Dialogflow response messages.""" texts = [] for msg in response.query_result.response_messages: if hasattr(msg, "text") and msg.text.text: texts.extend(msg.text.text) return " ".join(texts) if texts else "" @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((InternalServerError, ServiceUnavailable)), reraise=True, ) async def detect_intent( self, session_id: str, request_dto: DetectIntentRequestDTO ) -> DetectIntentResponseDTO: """ Detect intent from user input using Dialogflow CX. Args: session_id: Unique session identifier request_dto: Detect intent request Returns: Detect intent response with query results Raises: GoogleAPIError: If Dialogflow API call fails """ if not session_id: raise ValueError("Session ID cannot be empty") if not request_dto: raise ValueError("Request DTO cannot be None") logger.info(f"Initiating detectIntent for session: {session_id}") try: # Build request session_path = self._build_session_path(session_id) query_input = self._map_query_input(request_dto.query_input) query_params = self._map_query_params(request_dto.query_params) detect_request = DetectIntentRequest( session=session_path, query_input=query_input, query_params=query_params, ) # Call Dialogflow logger.debug( f"Calling Dialogflow CX detectIntent for session: {session_id}" ) response = await self.client.detect_intent(request=detect_request) # Extract response data query_result = response.query_result response_text = self._extract_response_text(response) # Map to DTO query_result_dto = QueryResultDTO( responseText=response_text, parameters=dict(query_result.parameters) if query_result.parameters else None, ) result = DetectIntentResponseDTO( responseId=response.response_id, queryResult=query_result_dto, ) logger.info( f"Successfully processed detectIntent for session: {session_id}" ) return result except GoogleAPIError as e: logger.error( f"Dialogflow CX API error for session {session_id}: {e.message}", exc_info=True, ) raise except Exception as e: logger.error( f"Unexpected error in detectIntent for session {session_id}: {str(e)}", exc_info=True, ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((InternalServerError, ServiceUnavailable)), reraise=True, ) async def detect_intent_event( self, session_id: str, event_name: str, parameters: dict | None = None, language_code: str | None = None, ) -> DetectIntentResponseDTO: """ Trigger Dialogflow event detection. Used for notification events and system-triggered flows. Args: session_id: Unique session identifier event_name: Dialogflow event name (e.g., "notificacion") parameters: Event parameters language_code: Language code (defaults to settings) Returns: Detect intent response Raises: GoogleAPIError: If Dialogflow API call fails """ if not session_id: raise ValueError("Session ID cannot be empty") if not event_name: raise ValueError("Event name cannot be empty") lang_code = language_code or self.default_language logger.info( f"Triggering Dialogflow event '{event_name}' for session: {session_id}" ) try: # Build request session_path = self._build_session_path(session_id) query_input = QueryInput( event=EventInput(event=event_name), language_code=lang_code, ) query_params = None if parameters: query_params = QueryParameters(parameters=parameters) detect_request = DetectIntentRequest( session=session_path, query_input=query_input, query_params=query_params, ) # Call Dialogflow logger.debug( f"Calling Dialogflow CX for event '{event_name}' in session: {session_id}" ) response = await self.client.detect_intent(request=detect_request) # Extract response data query_result = response.query_result response_text = self._extract_response_text(response) # Map to DTO query_result_dto = QueryResultDTO( responseText=response_text, parameters=dict(query_result.parameters) if query_result.parameters else None, ) result = DetectIntentResponseDTO( responseId=response.response_id, queryResult=query_result_dto, ) logger.info( f"Successfully processed event '{event_name}' for session: {session_id}" ) return result except GoogleAPIError as e: logger.error( f"Dialogflow CX API error for event '{event_name}' in session {session_id}: {e.message}", exc_info=True, ) raise except Exception as e: logger.error( f"Unexpected error triggering event '{event_name}' for session {session_id}: {str(e)}", exc_info=True, ) raise async def close(self): """Close the Dialogflow client.""" await self.client.transport.close() logger.info("Dialogflow CX SessionsClient closed")