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
+5
View File
@@ -23,4 +23,9 @@ class ChatController extends Controller
{
return view('chat.configuracion');
}
public function iaLogs()
{
return view('chat.ia-logs');
}
}
@@ -31,13 +31,14 @@ class TelegramWebhookController extends Controller
// Voice / audio messages
if (isset($update['message']['voice']) || isset($update['message']['audio'])) {
$chatId = (string) $update['message']['chat']['id'];
$fileId = $update['message']['voice']['file_id'] ?? $update['message']['audio']['file_id'];
$nombre = trim(
$chatId = (string) $update['message']['chat']['id'];
$fileId = $update['message']['voice']['file_id'] ?? $update['message']['audio']['file_id'];
$duration = $update['message']['voice']['duration'] ?? $update['message']['audio']['duration'] ?? null;
$nombre = trim(
($update['message']['from']['first_name'] ?? '') . ' ' .
($update['message']['from']['last_name'] ?? '')
);
$bot->handleVoice($chatId, $fileId, $nombre);
$bot->handleVoice($chatId, $fileId, $nombre, (int) $duration);
return response()->json(['ok' => true]);
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Livewire\Chat;
use App\Models\AiUsageLog;
use Livewire\Component;
use Livewire\WithPagination;
class ShowAiLogs extends Component
{
use WithPagination;
public string $filtroServicio = '';
public string $filtroCanal = '';
public string $filtroFechaDesde = '';
public string $filtroFechaHasta = '';
protected $queryString = ['filtroServicio', 'filtroCanal', 'filtroFechaDesde', 'filtroFechaHasta'];
public function updatingFiltroServicio(): void { $this->resetPage(); }
public function updatingFiltroCanal(): void { $this->resetPage(); }
public function updatingFiltroFechaDesde(): void { $this->resetPage(); }
public function updatingFiltroFechaHasta(): void { $this->resetPage(); }
public function render()
{
$query = AiUsageLog::query()->latest();
if ($this->filtroServicio) {
$query->where('servicio', $this->filtroServicio);
}
if ($this->filtroCanal) {
$query->where('canal', $this->filtroCanal);
}
if ($this->filtroFechaDesde) {
$query->whereDate('created_at', '>=', $this->filtroFechaDesde);
}
if ($this->filtroFechaHasta) {
$query->whereDate('created_at', '<=', $this->filtroFechaHasta);
}
$logs = $query->paginate(30);
// Resumen total (aplicando mismos filtros sin paginación)
$resumenQuery = AiUsageLog::query();
if ($this->filtroServicio) $resumenQuery->where('servicio', $this->filtroServicio);
if ($this->filtroCanal) $resumenQuery->where('canal', $this->filtroCanal);
if ($this->filtroFechaDesde) $resumenQuery->whereDate('created_at', '>=', $this->filtroFechaDesde);
if ($this->filtroFechaHasta) $resumenQuery->whereDate('created_at', '<=', $this->filtroFechaHasta);
$resumen = $resumenQuery->selectRaw('
COUNT(*) as total_llamadas,
SUM(CASE WHEN resultado = "ok" THEN 1 ELSE 0 END) as total_ok,
SUM(CASE WHEN resultado = "error" THEN 1 ELSE 0 END) as total_errores,
SUM(input_tokens) as total_input_tokens,
SUM(output_tokens) as total_output_tokens,
SUM(audio_segundos) as total_segundos_whisper,
SUM(CASE WHEN servicio = "whisper" THEN costo ELSE 0 END) as costo_whisper,
AVG(tiempo_ms) as avg_tiempo_ms
')->first();
return view('livewire.chat.show-ai-logs', compact('logs', 'resumen'));
}
}
+24
View File
@@ -16,6 +16,7 @@ class ShowConfiguracionChat extends Component
public string $mensaje_bienvenida = '';
public string $mensaje_transferencia = '';
public string $webhookResult = '';
public string $whisperResult = '';
// ── Gemini IA ─────────────────────────────────────────────
public string $gemini_api_key = '';
@@ -85,6 +86,29 @@ class ShowConfiguracionChat extends Component
$this->alert('success', 'Configuracion guardada correctamente.');
}
public function resetearWhisper(): void
{
$url = trim($this->whisper_url ?: ChatConfig::get('whisper_url'));
$token = trim($this->whisper_token ?: ChatConfig::get('whisper_token'));
if (! $url) {
$this->whisperResult = '⚠️ La URL de Whisper está vacía.';
return;
}
try {
$response = Http::timeout(10)
->withBasicAuth('whisper', $token)
->get(rtrim($url, '/asr')); // ping al servidor
$this->whisperResult = $response->successful()
? '✅ Conexión con Whisper establecida correctamente.'
: '❌ Whisper respondió con error ' . $response->status() . '.';
} catch (\Throwable $e) {
$this->whisperResult = '❌ No se pudo conectar con Whisper: ' . $e->getMessage();
}
}
public function registrarWebhook(): void
{
$token = trim($this->telegram_token);