diff --git a/app/Services/GeminiVisionService.php b/app/Services/GeminiVisionService.php index 75787ad..50b2754 100644 --- a/app/Services/GeminiVisionService.php +++ b/app/Services/GeminiVisionService.php @@ -10,33 +10,14 @@ use Illuminate\Support\Facades\Log; class GeminiVisionService { private string $apiKey; - private string $apiUrl; - private string $model; public function __construct() { $this->apiKey = ChatConfig::get('gemini_api_key', ''); $this->model = ChatConfig::get('gemini_model', 'gemini-3.5-flash'); - $ver = preg_match('/gemini-(2\.5|3[\.\d]*)/', $this->model) ? 'v1' : 'v1beta'; - $this->apiUrl = "https://generativelanguage.googleapis.com/{$ver}/models/{$this->model}:generateContent"; } - private function buildRequest(array $contents, array $genConfig = []): array - { - return [ - 'contents' => $contents, - 'generationConfig' => array_merge(['temperature' => 0.1, 'maxOutputTokens' => 200], $genConfig), - ]; - } - - /** - * 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) { @@ -44,67 +25,74 @@ class GeminiVisionService return null; } - $prompt = $this->buildPrompt(); - $inicio = microtime(true); + $contents = [[ + 'parts' => [ + ['text' => $this->buildPrompt()], + ['inline_data' => ['mime_type' => $mimeType, 'data' => $base64]], + ], + ]]; - try { - $contents = [[ - 'parts' => [ - ['text' => $prompt], - ['inline_data' => ['mime_type' => $mimeType, 'data' => $base64]], - ], - ]]; + $body = [ + 'contents' => $contents, + 'generationConfig' => ['temperature' => 0.1, 'maxOutputTokens' => 200], + ]; - $response = Http::withHeaders(['Content-Type' => 'application/json']) - ->timeout(15) - ->post("{$this->apiUrl}?key={$this->apiKey}", $this->buildRequest($contents)); + $inicio = microtime(true); + $response = null; + $lastError = ''; - $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); + foreach (['v1', 'v1beta'] as $ver) { + $url = "https://generativelanguage.googleapis.com/{$ver}/models/{$this->model}:generateContent"; + try { + $resp = Http::withHeaders(['Content-Type' => 'application/json']) + ->timeout(20) + ->post("{$url}?key={$this->apiKey}", $body); - 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; + if ($resp->successful()) { + $response = $resp; + break; + } + $lastError = "[{$ver}] " . ($resp->json('error.message') ?? substr($resp->body(), 0, 200)); + } catch (\Throwable $e) { + $lastError = "[{$ver}] " . $e->getMessage(); } - - $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; } + + $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); + + if (! $response) { + Log::warning('[Gemini Vision] API failed: ' . $lastError); + AiUsageLog::registrar([ + 'servicio' => 'gemini_vision', + 'canal' => $canal, + 'usuario_id' => $usuarioId, + 'tiempo_ms' => $tiempoMs, + 'resultado' => 'error', + 'detalle' => ['error' => $lastError, 'modelo' => $this->model], + ]); + // Return error info so caller can show it + return ['__error' => $lastError]; + } + + $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), 'modelo' => $this->model], + ]); + + return $resultado; } private function buildPrompt(): string @@ -130,21 +118,13 @@ PROMPT; private function parseRespuesta(string $text): ?array { - $text = trim($text); - $text = preg_replace('/```json\s*|\s*```/', '', $text); - $text = trim($text); - + $text = trim(preg_replace('/```json\s*|\s*```/', '', $text)); $json = json_decode($text, true); - if (! is_array($json)) { + if (! is_array($json) || isset($json['error'])) { 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']); } diff --git a/app/Services/TelegramBotService.php b/app/Services/TelegramBotService.php index 6f71406..2b6b1ad 100644 --- a/app/Services/TelegramBotService.php +++ b/app/Services/TelegramBotService.php @@ -187,6 +187,11 @@ class TelegramBotService return; } + if (isset($datosPago['__error'])) { + $this->send($chatId, "⚠️ Error al analizar el comprobante:\n`" . $datosPago['__error'] . "`"); + return; + } + $resultado = app(PagoValidadorService::class)->validar($datosPago); $monto = number_format($datosPago['valor'] ?? 0); $ref = $datosPago['referencia'] ?? 'N/A';