delete test
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Servicio de Procesamiento de Lenguaje Natural (NLP)
|
||||
* Detecta intenciones y palabras clave en los mensajes
|
||||
*/
|
||||
|
||||
class NLPService
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa un mensaje y encuentra la mejor respuesta automática
|
||||
* @param string $message
|
||||
* @param string $phoneNumber
|
||||
* @return array|null ['type' => 'autoresponse|menu|transfer', 'data' => array]
|
||||
*/
|
||||
public function processMessage(string $message, string $phoneNumber): ?array
|
||||
{
|
||||
// Normalizar mensaje
|
||||
$normalizedMessage = $this->normalizeText($message);
|
||||
|
||||
// 1. Verificar si es una opción de menú numérica
|
||||
if (is_numeric(trim($message))) {
|
||||
return $this->processMenuOption($message, $phoneNumber);
|
||||
}
|
||||
|
||||
// 2. Buscar respuesta automática por palabra clave
|
||||
$response = $this->findAutoResponse($normalizedMessage);
|
||||
|
||||
if ($response) {
|
||||
return $this->formatResponse($response);
|
||||
}
|
||||
|
||||
// 3. Si no encuentra nada, devolver respuesta por defecto
|
||||
return $this->getDefaultResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza el texto para comparación
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function normalizeText(string $text): string
|
||||
{
|
||||
// Convertir a minúsculas
|
||||
$text = mb_strtolower($text, 'UTF-8');
|
||||
|
||||
// Remover acentos
|
||||
$text = $this->removeAccents($text);
|
||||
|
||||
// Remover emojis y caracteres especiales (excepto espacios y comas)
|
||||
$text = preg_replace('/[^\p{L}\p{N}\s,]/u', '', $text);
|
||||
|
||||
// Remover espacios extras
|
||||
$text = preg_replace('/\s+/', ' ', $text);
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remueve acentos de un texto
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function removeAccents(string $text): string
|
||||
{
|
||||
$accents = [
|
||||
'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u',
|
||||
'Á' => 'a', 'É' => 'e', 'Í' => 'i', 'Ó' => 'o', 'Ú' => 'u',
|
||||
'ñ' => 'n', 'Ñ' => 'n', 'ü' => 'u', 'Ü' => 'u'
|
||||
];
|
||||
|
||||
return strtr($text, $accents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca la mejor respuesta automática para el mensaje
|
||||
* @param string $normalizedMessage
|
||||
* @return array|null
|
||||
*/
|
||||
private function findAutoResponse(string $normalizedMessage): ?array
|
||||
{
|
||||
// Obtener todas las respuestas automáticas activas ordenadas por prioridad
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT * FROM autoresponses
|
||||
WHERE is_active = 1
|
||||
AND trigger_type != 'welcome'
|
||||
ORDER BY priority DESC, id ASC
|
||||
");
|
||||
$stmt->execute();
|
||||
$autoResponses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($autoResponses as $response) {
|
||||
if ($this->matchesTrigger($normalizedMessage, $response)) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un mensaje coincide con un trigger
|
||||
* @param string $message
|
||||
* @param array $response
|
||||
* @return bool
|
||||
*/
|
||||
private function matchesTrigger(string $message, array $response): bool
|
||||
{
|
||||
$triggerType = $response['trigger_type'];
|
||||
$triggerValue = $response['trigger_value'];
|
||||
|
||||
switch ($triggerType) {
|
||||
case 'keyword':
|
||||
return $this->matchesKeywords($message, $triggerValue);
|
||||
|
||||
case 'contains':
|
||||
return $this->containsPhrase($message, $triggerValue);
|
||||
|
||||
case 'exact':
|
||||
return $this->exactMatch($message, $triggerValue);
|
||||
|
||||
case 'default':
|
||||
return true; // Siempre coincide (pero tiene prioridad baja)
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el mensaje contiene alguna palabra clave
|
||||
* @param string $message
|
||||
* @param string $keywordsString Palabras separadas por comas
|
||||
* @return bool
|
||||
*/
|
||||
private function matchesKeywords(string $message, string $keywordsString): bool
|
||||
{
|
||||
if (empty($keywordsString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$keywords = array_map('trim', explode(',', $keywordsString));
|
||||
$keywords = array_map([$this, 'normalizeText'], $keywords);
|
||||
|
||||
$messageWords = explode(' ', $message);
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
// Verificar si la palabra clave completa está en el mensaje
|
||||
if (strpos($message, $keyword) !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// O si alguna palabra del mensaje coincide exactamente
|
||||
if (in_array($keyword, $messageWords)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el mensaje contiene una frase
|
||||
* @param string $message
|
||||
* @param string $phrase
|
||||
* @return bool
|
||||
*/
|
||||
private function containsPhrase(string $message, string $phrase): bool
|
||||
{
|
||||
$normalizedPhrase = $this->normalizeText($phrase);
|
||||
return strpos($message, $normalizedPhrase) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica coincidencia exacta
|
||||
* @param string $message
|
||||
* @param string $exact
|
||||
* @return bool
|
||||
*/
|
||||
private function exactMatch(string $message, string $exact): bool
|
||||
{
|
||||
$normalizedExact = $this->normalizeText($exact);
|
||||
return $message === $normalizedExact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa una opción numérica de menú
|
||||
* @param string $option
|
||||
* @param string $phoneNumber
|
||||
* @return array|null
|
||||
*/
|
||||
private function processMenuOption(string $option, string $phoneNumber): ?array
|
||||
{
|
||||
$stateService = new ConversationStateService();
|
||||
$currentMenuId = $stateService->getCurrentMenuId($phoneNumber);
|
||||
|
||||
if (!$currentMenuId) {
|
||||
// Si no hay menú actual, buscar el menú principal
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT id FROM menus
|
||||
WHERE menu_key = 'lab_menu_principal'
|
||||
AND status = 'active'
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute();
|
||||
$menu = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$currentMenuId = $menu ? $menu['id'] : null;
|
||||
}
|
||||
|
||||
if (!$currentMenuId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Buscar la opción en el menú actual
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT * FROM menu_options
|
||||
WHERE menu_id = ?
|
||||
AND option_key = ?
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$currentMenuId, trim($option)]);
|
||||
$menuOption = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$menuOption) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->formatMenuOption($menuOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea una respuesta automática
|
||||
* @param array $response
|
||||
* @return array
|
||||
*/
|
||||
private function formatResponse(array $response): array
|
||||
{
|
||||
$result = [
|
||||
'type' => $response['response_type'],
|
||||
'priority' => $response['priority']
|
||||
];
|
||||
|
||||
switch ($response['response_type']) {
|
||||
case 'text':
|
||||
$result['message'] = $response['response_text'];
|
||||
break;
|
||||
|
||||
case 'menu':
|
||||
$result['menu_id'] = $response['menu_id'];
|
||||
break;
|
||||
|
||||
case 'template':
|
||||
$result['template_name'] = $response['template_name'] ?? null;
|
||||
break;
|
||||
|
||||
case 'transfer':
|
||||
$result['message'] = $response['response_text'];
|
||||
$result['transfer'] = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea una opción de menú
|
||||
* @param array $menuOption
|
||||
* @return array
|
||||
*/
|
||||
private function formatMenuOption(array $menuOption): array
|
||||
{
|
||||
$result = [
|
||||
'type' => $menuOption['response_type']
|
||||
];
|
||||
|
||||
switch ($menuOption['response_type']) {
|
||||
case 'text':
|
||||
$result['message'] = $menuOption['response_text'];
|
||||
break;
|
||||
|
||||
case 'menu':
|
||||
$result['menu_id'] = $menuOption['next_menu_id'];
|
||||
break;
|
||||
|
||||
case 'transfer':
|
||||
$result['message'] = $menuOption['response_text'];
|
||||
$result['transfer'] = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la respuesta por defecto
|
||||
* @return array
|
||||
*/
|
||||
private function getDefaultResponse(): array
|
||||
{
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT * FROM autoresponses
|
||||
WHERE trigger_type = 'default'
|
||||
AND is_active = 1
|
||||
ORDER BY priority DESC
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute();
|
||||
$response = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($response) {
|
||||
return $this->formatResponse($response);
|
||||
}
|
||||
|
||||
// Respuesta fallback hardcoded
|
||||
return [
|
||||
'type' => 'text',
|
||||
'message' => "❓ No he comprendido tu mensaje.\n\nEscribe MENÚ para ver las opciones disponibles."
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta el sentimiento del mensaje (básico)
|
||||
* @param string $message
|
||||
* @return string positive|negative|neutral
|
||||
*/
|
||||
public function detectSentiment(string $message): string
|
||||
{
|
||||
$normalized = $this->normalizeText($message);
|
||||
|
||||
// Palabras positivas
|
||||
$positiveWords = ['gracias', 'excelente', 'perfecto', 'bien', 'bueno', 'genial', 'ok', 'vale'];
|
||||
|
||||
// Palabras negativas / frustración
|
||||
$negativeWords = ['no funciona', 'problema', 'error', 'mal', 'ayuda', 'no entiendo', 'dificil', 'complicado'];
|
||||
|
||||
$positiveCount = 0;
|
||||
$negativeCount = 0;
|
||||
|
||||
foreach ($positiveWords as $word) {
|
||||
if (strpos($normalized, $word) !== false) {
|
||||
$positiveCount++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($negativeWords as $word) {
|
||||
if (strpos($normalized, $word) !== false) {
|
||||
$negativeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($negativeCount > $positiveCount) {
|
||||
return 'negative';
|
||||
} elseif ($positiveCount > 0) {
|
||||
return 'positive';
|
||||
}
|
||||
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determina si el mensaje indica frustración (para escalar a asesor)
|
||||
* @param string $message
|
||||
* @return bool
|
||||
*/
|
||||
public function detectsFrustration(string $message): bool
|
||||
{
|
||||
return $this->detectSentiment($message) === 'negative';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user