- Chat público en /chat con flujos de compra, recarga y credenciales - Verificación de identidad por código OTP enviado al correo registrado - Botones enriquecidos: cards, links, credenciales, validación de comprobante - Gemini IA para detección de intención en texto libre - Gemini Vision para extraer datos de foto de comprobante - Validador de pagos cruzando IA con correos IMAP (Bancolombia) - MercadoPago como pasarela de pago (reemplaza Wompi en el chat) - Migraciones: payload en chat_messages, user_id en chat_contacts - Config admin: API key Gemini, toggles IA y validación de foto Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
3.8 KiB
PHP
112 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
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-1.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): ?array
|
|
{
|
|
if (! $this->apiKey || ChatConfig::get('gemini_habilitado', '0') !== '1') {
|
|
return null;
|
|
}
|
|
|
|
$prompt = $this->buildPrompt($texto);
|
|
|
|
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,
|
|
],
|
|
]);
|
|
|
|
if (! $response->successful()) {
|
|
Log::warning('[Gemini Intent] API error: ' . $response->body());
|
|
return null;
|
|
}
|
|
|
|
$text = $response->json('candidates.0.content.parts.0.text', '');
|
|
return $this->parseRespuesta($text);
|
|
|
|
} catch (\Throwable $e) {
|
|
Log::warning('[Gemini Intent] Excepcion: ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function buildPrompt(string $texto): string
|
|
{
|
|
$promptExtra = ChatConfig::get('gemini_prompt_extra', '');
|
|
|
|
return <<<PROMPT
|
|
Eres un clasificador de intenciones para una tienda de streaming (peliculas, series).
|
|
{$promptExtra}
|
|
|
|
Acciones validas (devuelve EXACTAMENTE una de estas en el campo "action"):
|
|
- menu.principal (usuario quiere ver el menu, inicio, ayuda general)
|
|
- servicios.listar (quiere ver servicios, planes, catalogo, que hay disponible)
|
|
- promo.listar (quiere ver promociones, ofertas, descuentos)
|
|
- recarga.iniciar (quiere recargar saldo, agregar dinero, cargar cuenta)
|
|
- credenciales.listar (quiere ver sus credenciales, usuario, contrasena, cuenta, acceso)
|
|
- historial.ver (quiere ver historial, compras anteriores, que ha comprado)
|
|
- perfil.ver (quiere ver su perfil, sus datos, su informacion)
|
|
- asesor.solicitar (quiere hablar con una persona, agente, asesor, humano, soporte)
|
|
|
|
Texto del usuario: "{$texto}"
|
|
|
|
Responde UNICAMENTE con un JSON valido, sin markdown, sin explicacion:
|
|
{"action": "la_accion", "data": {}}
|
|
|
|
Si no puedes determinar la intencion con certeza, 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'] : [],
|
|
];
|
|
}
|
|
}
|