Add AI usage logs for Gemini and Whisper

- Migration + AiUsageLog model with tokens, duration, time, cost fields
- GeminiIntentService and GeminiVisionService log every API call
  with input/output token counts from usageMetadata and response time
- TelegramBotService logs Whisper calls with audio duration (from Telegram)
  and calculates cost at $1/second
- Whisper voice duration now passed from TelegramWebhookController
- Free text in Telegram now tries Gemini intent detection before showing menu
- /chat/ia-logs: Livewire component with summary cards + filterable table
- Whisper connection test button in config panel
- "Logs IA" link added to Chat/Bot nav section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro
2026-07-14 19:13:59 +00:00
co-authored by Claude Sonnet 4.6
parent ff109a5608
commit 44297aa0bb
14 changed files with 519 additions and 26 deletions
+39 -3
View File
@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\AiUsageLog;
use App\Models\ChatConfig;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@@ -23,7 +24,7 @@ class GeminiVisionService
* @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): ?array
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.');
@@ -31,6 +32,7 @@ class GeminiVisionService
}
$prompt = $this->buildPrompt();
$inicio = microtime(true);
try {
$response = Http::withHeaders(['Content-Type' => 'application/json'])
@@ -48,16 +50,50 @@ class GeminiVisionService
],
]);
$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;
}
$text = $response->json('candidates.0.content.parts.0.text', '');
return $this->parseRespuesta($text);
$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;
}
}