286 lines
9.3 KiB
Python
286 lines
9.3 KiB
Python
"""
|
|
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")
|