Files
sirpremiumv2/app/Services/GeminiVisionService.php
T
LizandroandClaude Sonnet 4.6 75eddd16a6 feat: make Gemini model configurable + auto-list available models on error
- gemini_model is now a saved config field (default: gemini-2.0-flash)
- Config panel shows a text input to set the model name
- probarGemini() reads the configured model and on error calls ListModels
  to show exactly which model names are available for the account's API key
- GeminiIntentService and GeminiVisionService build the URL from config at runtime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 20:11:32 +00:00

155 lines
5.5 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 GeminiVisionService
{
private string $apiKey;
private string $apiUrl;
public function __construct()
{
$this->apiKey = ChatConfig::get('gemini_api_key', '');
$model = ChatConfig::get('gemini_model', 'gemini-2.0-flash');
$this->apiUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent";
}
/**
* Extrae datos de pago de una imagen de comprobante.
*
* @param string $base64 Imagen en base64
* @param string $mimeType MIME type (image/jpeg, image/png, etc.)
* @return array|null ['banco', 'valor', 'referencia', 'fecha', 'hora', 'remitente'] o null
*/
public function extraerPago(string $base64, string $mimeType, ?int $usuarioId = null, string $canal = 'telegram'): ?array
{
if (! $this->apiKey) {
Log::warning('[Gemini Vision] No hay API key configurada.');
return null;
}
$prompt = $this->buildPrompt();
$inicio = microtime(true);
try {
$response = Http::withHeaders(['Content-Type' => 'application/json'])
->timeout(15)
->post("{$this->apiUrl}?key={$this->apiKey}", [
'contents' => [[
'parts' => [
['text' => $prompt],
['inline_data' => ['mime_type' => $mimeType, 'data' => $base64]],
],
]],
'generationConfig' => [
'temperature' => 0.1,
'maxOutputTokens' => 200,
],
]);
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
if (! $response->successful()) {
Log::warning('[Gemini Vision] API error: ' . $response->body());
AiUsageLog::registrar([
'servicio' => 'gemini_vision',
'canal' => $canal,
'usuario_id'=> $usuarioId,
'tiempo_ms' => $tiempoMs,
'resultado' => 'error',
'detalle' => ['error' => substr($response->body(), 0, 300)],
]);
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_vision',
'canal' => $canal,
'usuario_id' => $usuarioId,
'input_tokens' => $inputTokens,
'output_tokens'=> $outputTokens,
'tiempo_ms' => $tiempoMs,
'resultado' => $resultado ? 'ok' : 'error',
'detalle' => $resultado ? ['banco' => $resultado['banco'], 'valor' => $resultado['valor']] : ['raw' => substr($text, 0, 200)],
]);
return $resultado;
} catch (\Throwable $e) {
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
Log::error('[Gemini Vision] Excepcion: ' . $e->getMessage());
AiUsageLog::registrar([
'servicio' => 'gemini_vision',
'canal' => $canal,
'usuario_id'=> $usuarioId,
'tiempo_ms' => $tiempoMs,
'resultado' => 'error',
'detalle' => ['error' => $e->getMessage()],
]);
return null;
}
}
private function buildPrompt(): string
{
return <<<PROMPT
Analiza esta imagen de comprobante de pago bancario o transferencia.
Extrae los siguientes campos:
- banco: nombre del banco (Bancolombia, Nequi, Davivienda, etc.)
- valor: monto en numeros enteros sin simbolos ni puntos (ej: 50000)
- referencia: numero de referencia o transaccion (si existe)
- fecha: fecha en formato dd/mm/yyyy
- hora: hora en formato HH:mm (si existe)
- remitente: nombre de quien envia el dinero (si aparece)
Responde UNICAMENTE con un JSON valido, sin markdown, sin explicacion:
{"banco": "...", "valor": 50000, "referencia": "...", "fecha": "...", "hora": "...", "remitente": "..."}
Si no encuentras un campo, usa null para ese campo.
Si la imagen NO es un comprobante de pago, responde exactamente: {"error": "no_es_comprobante"}
PROMPT;
}
private function parseRespuesta(string $text): ?array
{
$text = trim($text);
$text = preg_replace('/```json\s*|\s*```/', '', $text);
$text = trim($text);
$json = json_decode($text, true);
if (! is_array($json)) {
return null;
}
if (isset($json['error'])) {
return null;
}
// Normalizar valor a entero
if (isset($json['valor'])) {
$json['valor'] = (int) preg_replace('/[^0-9]/', '', (string) $json['valor']);
}
return [
'banco' => $json['banco'] ?? null,
'valor' => $json['valor'] ?? null,
'referencia' => $json['referencia'] ?? null,
'fecha' => $json['fecha'] ?? null,
'hora' => $json['hora'] ?? null,
'remitente' => $json['remitente'] ?? null,
];
}
}