Files
sirpremiumv2/app/Services/GeminiIntentService.php
T
2026-07-14 20:07:52 +00:00

161 lines
6.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\AiUsageLog;
use App\Models\ChatConfig;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class GeminiIntentService
{
private string $apiKey;
private string $apiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
public function __construct()
{
$this->apiKey = ChatConfig::get('gemini_api_key', '');
}
/**
* Detecta la intención del texto del usuario y la mapea a una acción del sistema.
* Retorna ['action' => string, 'data' => array] o null si no se puede detectar.
*/
public function detectar(string $texto, ?int $usuarioId = null, string $canal = 'web'): ?array
{
if (! $this->apiKey || ChatConfig::get('gemini_habilitado', '0') !== '1') {
return null;
}
$prompt = $this->buildPrompt($texto);
$inicio = microtime(true);
try {
$response = Http::withHeaders(['Content-Type' => 'application/json'])
->timeout(8)
->post("{$this->apiUrl}?key={$this->apiKey}", [
'contents' => [['parts' => [['text' => $prompt]]]],
'generationConfig' => [
'temperature' => 0.1,
'maxOutputTokens' => 100,
],
]);
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
if (! $response->successful()) {
Log::warning('[Gemini Intent] API error: ' . $response->body());
AiUsageLog::registrar([
'servicio' => 'gemini_intent',
'canal' => $canal,
'usuario_id'=> $usuarioId,
'tiempo_ms' => $tiempoMs,
'resultado' => 'error',
'detalle' => ['error' => substr($response->body(), 0, 300), 'texto' => $texto],
]);
return null;
}
$inputTokens = $response->json('usageMetadata.promptTokenCount', 0);
$outputTokens = $response->json('usageMetadata.candidatesTokenCount', 0);
$text = $response->json('candidates.0.content.parts.0.text', '');
$resultado = $this->parseRespuesta($text);
AiUsageLog::registrar([
'servicio' => 'gemini_intent',
'canal' => $canal,
'usuario_id' => $usuarioId,
'input_tokens' => $inputTokens,
'output_tokens'=> $outputTokens,
'tiempo_ms' => $tiempoMs,
'resultado' => 'ok',
'detalle' => ['texto' => $texto, 'accion' => $resultado['action'] ?? null],
]);
return $resultado;
} catch (\Throwable $e) {
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
Log::warning('[Gemini Intent] Excepcion: ' . $e->getMessage());
AiUsageLog::registrar([
'servicio' => 'gemini_intent',
'canal' => $canal,
'usuario_id'=> $usuarioId,
'tiempo_ms' => $tiempoMs,
'resultado' => 'error',
'detalle' => ['error' => $e->getMessage(), 'texto' => $texto],
]);
return null;
}
}
private function buildPrompt(string $texto): string
{
$promptExtra = ChatConfig::get('gemini_prompt_extra', '');
return <<<PROMPT
Eres un clasificador de intenciones para SirPremium, una tienda colombiana de streaming (Netflix, Disney+, HBO, Spotify, etc.).
Los usuarios escriben en español latinoamericano informal, con jerga colombiana y errores ortográficos.
{$promptExtra}
Acciones válidas — devuelve EXACTAMENTE una de estas:
- menu.principal → saludo, "hola", "inicio", "volver", "menú", pedir ayuda general
- servicios.listar → ver planes, catálogo, qué servicios hay, "quiero Netflix", "qué tienen", "cuánto vale", preguntar por precios
- promo.listar → ver promociones, ofertas, combos, descuentos, "hay algo especial"
- recarga.iniciar → recargar saldo, cargar dinero, "quiero poner saldo", "cómo recargo", "agregar plata", "depositar"
- credenciales.listar → ver contraseña, cómo entrar, "no puedo acceder", "dame el usuario", "mis datos de Netflix", "cómo me conecto"
- historial.ver → historial de compras, "qué he comprado", "mis pedidos", "mis servicios activos"
- perfil.ver → ver perfil, mis datos personales, cambiar información
- asesor.solicitar → hablar con persona, "quiero un asesor", "necesito ayuda de alguien", "soporte humano"
Ejemplos de clasificación:
"hacer recarga" → recarga.iniciar
"quiero recargar" → recarga.iniciar
"cargar saldo" → recarga.iniciar
"poner plata" → recarga.iniciar
"ver planes" → servicios.listar
"qué tienen de Netflix" → servicios.listar
"hay promociones" → promo.listar
"mis contraseñas" → credenciales.listar
"no puedo entrar a mi cuenta" → credenciales.listar
"hablar con alguien" → asesor.solicitar
Texto del usuario: "{$texto}"
Responde ÚNICAMENTE con JSON válido, sin markdown, sin explicación:
{"action": "la_accion", "data": {}}
Si el texto es completamente ambiguo o irrelevante, usa: {"action": "menu.principal", "data": {}}
PROMPT;
}
private function parseRespuesta(string $text): ?array
{
$text = trim($text);
// Limpiar markdown si Gemini lo incluye
$text = preg_replace('/```json\s*|\s*```/', '', $text);
$text = trim($text);
$json = json_decode($text, true);
if (! is_array($json) || empty($json['action'])) {
return null;
}
$accionesValidas = [
'menu.principal', 'servicios.listar', 'promo.listar',
'recarga.iniciar', 'credenciales.listar', 'historial.ver',
'perfil.ver', 'asesor.solicitar',
];
if (! in_array($json['action'], $accionesValidas)) {
return ['action' => 'menu.principal', 'data' => []];
}
return [
'action' => $json['action'],
'data' => is_array($json['data'] ?? null) ? $json['data'] : [],
];
}
}