feat: agente conversacional con extracción de parámetros (GeminiAgentService)
- Nuevo GeminiAgentService: entiende lenguaje natural, responde conversacionalmente y extrae acción + parámetros (ej: servicio:"Netflix", monto:50000) - handleFreeText usa GeminiAgentService en lugar de GeminiIntentService - dispatchAction acepta accion_data: va directo a Netflix sin pasar por el listado - viewServiceByName: busca servicio por nombre (ILIKE) y abre sus planes - Keywords simplificadas: solo intents genéricos sin parámetros Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1adfffd907
commit
656b32c706
@@ -30,6 +30,7 @@ use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\GeminiAgentService;
|
||||
|
||||
class TelegramBotService
|
||||
{
|
||||
@@ -1279,13 +1280,14 @@ class TelegramBotService
|
||||
|
||||
private function detectarPorKeywords(string $texto): ?string
|
||||
{
|
||||
// Solo intents genéricos sin parámetros — los específicos (con nombre de servicio o monto)
|
||||
// los maneja GeminiAgentService para extraer parámetros y responder conversacionalmente.
|
||||
$patrones = [
|
||||
'recarga.iniciar' => '/recargar|recarg[ao]|cargar\s*saldo|poner\s*plata|depositar|agregar\s*plata|quiero\s*saldo|meter\s*plata/i',
|
||||
'servicios.listar' => '/servicios?|planes?|cat[aá]logo|qu[eé]\s*tienen|qu[eé]\s*hay|netflix|disney|hbo|max|prime|spotify|crunchyroll|paramount|precio|cu[aá]nto\s*vale|cu[aá]nto\s*cuesta/i',
|
||||
'promo.listar' => '/promo|oferta|combo|descuento|especial|barato|econom/i',
|
||||
'credenciales.listar' => '/credencial|contrase[ñn]a|usuario|no\s*puedo\s*entrar|no\s*accedo|mis\s*datos|c[oó]mo\s*(entro|accedo|me\s*conecto)|mi\s*(cuenta|netflix|spotify)/i',
|
||||
'historial.ver' => '/historial|compras?|pedidos?|servicios?\s*activos?|qu[eé]\s*compr[eé]|mis\s*servicios/i',
|
||||
'perfil.ver' => '/perfil|mis\s*datos|mi\s*informaci[oó]n|cambiar\s*datos/i',
|
||||
'recarga.iniciar' => '/\b(recargar?|recarg[ao]|cargar\s+saldo|poner\s+plata|depositar|agregar\s+plata|meter\s+plata)\b/i',
|
||||
'promo.listar' => '/\b(promo|oferta|combo|descuento)\b/i',
|
||||
'credenciales.listar' => '/\b(credencial|contrase[ñn]a)\b|no\s+puedo\s+entrar|mis\s+datos\s+de\s+\w/i',
|
||||
'historial.ver' => '/\b(historial|mis\s+compras?|mis\s+pedidos?)\b/i',
|
||||
'perfil.ver' => '/\bmi\s+perfil\b/i',
|
||||
];
|
||||
|
||||
foreach ($patrones as $accion => $patron) {
|
||||
@@ -1306,7 +1308,7 @@ class TelegramBotService
|
||||
|
||||
$texto = $state['data']['last_text'] ?? '';
|
||||
|
||||
// Detección rápida por palabras clave (más fiable que IA para frases coloquiales)
|
||||
// Detección rápida para intents genéricos sin parámetros
|
||||
if ($texto) {
|
||||
$accionLocal = $this->detectarPorKeywords($texto);
|
||||
if ($accionLocal) {
|
||||
@@ -1315,24 +1317,40 @@ class TelegramBotService
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Gemini para frases ambiguas
|
||||
// Agente conversacional: entiende lenguaje natural, extrae parámetros y responde
|
||||
if ($texto) {
|
||||
$accion = app(GeminiIntentService::class)->detectar($texto, $state['user_id'] ?? null, 'telegram');
|
||||
if ($accion && $accion['action'] !== 'menu.principal') {
|
||||
$this->dispatchAction($chatId, $accion['action'], $state);
|
||||
$contexto = GeminiAgentService::buildContexto($state, $state['user_id'] ?? null);
|
||||
$resultado = app(GeminiAgentService::class)->responder(
|
||||
$texto, $contexto, $state['user_id'] ?? null, 'telegram'
|
||||
);
|
||||
|
||||
if ($resultado['respuesta']) {
|
||||
$this->send($chatId, $resultado['respuesta']);
|
||||
}
|
||||
|
||||
if ($resultado['accion']) {
|
||||
$this->dispatchAction($chatId, $resultado['accion'], $state, $resultado['accion_data']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($resultado['respuesta']) {
|
||||
return; // El agente respondió la pregunta, sin acción que ejecutar
|
||||
}
|
||||
}
|
||||
|
||||
$this->showMainMenu($chatId);
|
||||
}
|
||||
|
||||
private function dispatchAction(string $chatId, string $action, array $state): void
|
||||
private function dispatchAction(string $chatId, string $action, array $state, array $accionData = []): void
|
||||
{
|
||||
match ($action) {
|
||||
'servicios.listar' => $this->listServices($chatId),
|
||||
'servicios.listar' => isset($accionData['servicio'])
|
||||
? $this->viewServiceByName($chatId, (string) $accionData['servicio'])
|
||||
: $this->listServices($chatId),
|
||||
'recarga.iniciar' => isset($accionData['monto']) && (int) $accionData['monto'] > 0
|
||||
? $this->chooseRechargeMethod($chatId, (int) $accionData['monto'])
|
||||
: $this->showRechargeAmounts($chatId),
|
||||
'promo.listar' => $this->listPromos($chatId),
|
||||
'recarga.iniciar' => $this->showRechargeAmounts($chatId),
|
||||
'credenciales.listar' => $this->showCredentials($chatId),
|
||||
'historial.ver' => $this->showHistory($chatId),
|
||||
'perfil.ver' => $this->showProfile($chatId),
|
||||
@@ -1341,6 +1359,17 @@ class TelegramBotService
|
||||
};
|
||||
}
|
||||
|
||||
private function viewServiceByName(string $chatId, string $nombre): void
|
||||
{
|
||||
$servicio = Servicio::whereRaw('LOWER(nombre) LIKE ?', ['%' . strtolower($nombre) . '%'])
|
||||
->where('estado', 'activo')
|
||||
->first();
|
||||
|
||||
$servicio
|
||||
? $this->viewService($chatId, $servicio->id)
|
||||
: $this->listServices($chatId);
|
||||
}
|
||||
|
||||
// ─── State management ─────────────────────────────────────
|
||||
|
||||
private function getState(string $chatId): array
|
||||
|
||||
Reference in New Issue
Block a user