feat: AI usage logging (AiLogger, ai_logs table, admin view)
- AiLogger.php: new service, inserts into ai_logs on every AI call - ai_logs table: added to migrate.php (provider, model, call_type, tokens, duration_ms) - AiBot: logs chat, media_response, nlu calls with timing and token counts - MediaTranscriber: logs whisper, geminiAudio, openaiVision, geminiVision, claudeVision - /admin/ai-logs: paginated table with provider/company filter - Layout.php: added IA Logs nav link - index.php: require AiLogger before AiBot; added GET /admin/ai-logs route Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1d9484a1e0
commit
5506ebd2af
@@ -54,6 +54,7 @@ class MediaTranscriber
|
||||
public static function transcribeAudio(string $mediaId, array $company): ?string
|
||||
{
|
||||
$cfg = self::getConfig($company);
|
||||
$cfg['__company_id'] = (int)($company['id'] ?? 0);
|
||||
$provider = $cfg['ai_provider'] ?? env('AI_PROVIDER', 'openai');
|
||||
|
||||
$media = self::downloadMedia($mediaId);
|
||||
@@ -75,6 +76,7 @@ class MediaTranscriber
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'wa_audio_') . '.' . $ext;
|
||||
file_put_contents($tmpFile, $bytes);
|
||||
|
||||
$t0 = (int)(microtime(true) * 1000);
|
||||
$ch = curl_init('https://api.openai.com/v1/audio/transcriptions');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
@@ -90,12 +92,17 @@ class MediaTranscriber
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$ms = (int)(microtime(true) * 1000) - $t0;
|
||||
@unlink($tmpFile);
|
||||
|
||||
if ($code !== 200 || !$resp) return null;
|
||||
|
||||
$data = json_decode($resp, true);
|
||||
return isset($data['text']) ? trim($data['text']) : null;
|
||||
$text = isset($data['text']) ? trim($data['text']) : null;
|
||||
if ($text !== null) {
|
||||
AiLogger::log($cfg['__company_id'] ?? null, 'openai', 'whisper-1', 'transcribe', '[audio]', $text, $ms);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function geminiAudio(string $bytes, string $mime, array $cfg): ?string
|
||||
@@ -115,6 +122,7 @@ class MediaTranscriber
|
||||
]);
|
||||
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
|
||||
$t0 = (int)(microtime(true) * 1000);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
@@ -126,12 +134,17 @@ class MediaTranscriber
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$ms = (int)(microtime(true) * 1000) - $t0;
|
||||
|
||||
if ($code !== 200 || !$resp) return null;
|
||||
|
||||
$data = json_decode($resp, true);
|
||||
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null;
|
||||
return $text !== null ? trim($text) : null;
|
||||
$text = $text !== null ? trim($text) : null;
|
||||
if ($text !== null) {
|
||||
AiLogger::log($cfg['__company_id'] ?? null, 'gemini', $model, 'transcribe', '[audio]', $text, $ms);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
// ── Imagen → intención/texto ──────────────────────────────────────────────
|
||||
@@ -139,6 +152,7 @@ class MediaTranscriber
|
||||
public static function extractFromImage(string $mediaId, array $company): ?string
|
||||
{
|
||||
$cfg = self::getConfig($company);
|
||||
$cfg['__company_id'] = (int)($company['id'] ?? 0);
|
||||
$provider = $cfg['ai_provider'] ?? env('AI_PROVIDER', 'openai');
|
||||
|
||||
$media = self::downloadMedia($mediaId);
|
||||
@@ -178,6 +192,7 @@ class MediaTranscriber
|
||||
]],
|
||||
]);
|
||||
|
||||
$t0 = (int)(microtime(true) * 1000);
|
||||
$ch = curl_init('https://api.openai.com/v1/chat/completions');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
@@ -189,12 +204,17 @@ class MediaTranscriber
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$ms = (int)(microtime(true) * 1000) - $t0;
|
||||
|
||||
if ($code !== 200 || !$resp) return null;
|
||||
|
||||
$data = json_decode($resp, true);
|
||||
$text = $data['choices'][0]['message']['content'] ?? null;
|
||||
return $text !== null ? trim($text) : null;
|
||||
$text = $text !== null ? trim($text) : null;
|
||||
if ($text !== null) {
|
||||
AiLogger::log($cfg['__company_id'] ?? null, 'openai', $model, 'vision', '[image]', $text, $ms);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function geminiVision(string $b64, string $mime, string $prompt, array $cfg): ?string
|
||||
@@ -213,6 +233,7 @@ class MediaTranscriber
|
||||
]);
|
||||
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
|
||||
$t0 = (int)(microtime(true) * 1000);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
@@ -224,12 +245,17 @@ class MediaTranscriber
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$ms = (int)(microtime(true) * 1000) - $t0;
|
||||
|
||||
if ($code !== 200 || !$resp) return null;
|
||||
|
||||
$data = json_decode($resp, true);
|
||||
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null;
|
||||
return $text !== null ? trim($text) : null;
|
||||
$text = $text !== null ? trim($text) : null;
|
||||
if ($text !== null) {
|
||||
AiLogger::log($cfg['__company_id'] ?? null, 'gemini', $model, 'vision', '[image]', $text, $ms);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function claudeVision(string $b64, string $mime, string $prompt, array $cfg): ?string
|
||||
@@ -250,6 +276,7 @@ class MediaTranscriber
|
||||
]],
|
||||
]);
|
||||
|
||||
$t0 = (int)(microtime(true) * 1000);
|
||||
$ch = curl_init('https://api.anthropic.com/v1/messages');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
@@ -261,12 +288,17 @@ class MediaTranscriber
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$ms = (int)(microtime(true) * 1000) - $t0;
|
||||
|
||||
if ($code !== 200 || !$resp) return null;
|
||||
|
||||
$data = json_decode($resp, true);
|
||||
$text = $data['content'][0]['text'] ?? null;
|
||||
return $text !== null ? trim($text) : null;
|
||||
$text = $text !== null ? trim($text) : null;
|
||||
if ($text !== null) {
|
||||
AiLogger::log($cfg['__company_id'] ?? null, 'claude', $model, 'vision', '[image]', $text, $ms);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user