AI Usage - every request made to an AI provider (ai_usage, * recorded by callAiApi() in functions/ai.php): when, the feature, who asked, * the client it was about, the model and provider, the tokens it took, how * long, how many passwords and keys were taken out of it first, and whether * it worked - why not, when it didn't. Totals for what is shown sit above. * Searched, and filtered by date, provider, model, feature and who; views for * the failed ones, the ones secrets were taken out of, and tests. Kept as long * as the audit log is (Admin > Settings > Logs). */ // Default Column Sortby Filter $sort = "ai_usage_id"; $order = "DESC"; require_once "includes/inc_all_admin.php"; $page_title = 'AI Usage'; // Only these sort - an unknown column would take the page down $sortable_columns = [ 'ai_usage_id' => "ai_usage_id $order", 'ai_usage_feature' => "ai_usage_feature $order, ai_usage_id DESC", 'user_name' => "user_name IS NULL, user_name $order, ai_usage_id DESC", 'client_name' => "client_name IS NULL, client_name $order, ai_usage_id DESC", 'ai_usage_model' => "ai_usage_model $order, ai_usage_id DESC", 'tokens' => "(ai_usage_tokens_in + ai_usage_tokens_out) $order, ai_usage_id DESC", 'ai_usage_ms' => "ai_usage_ms $order, ai_usage_id DESC", ]; if (!isset($sortable_columns[$sort])) { $sort = 'ai_usage_id'; } $features = aiFeatures(); $views = [ '' => ['label' => 'All', 'icon' => 'fa-list', 'where' => "ai_usage_feature <> 'test'", 'title' => 'Every request but tests'], 'failed' => ['label' => 'Failed', 'icon' => 'fa-times-circle', 'where' => "ai_usage_feature <> 'test' AND ai_usage_ok = 0", 'title' => 'The provider turned them away, or couldn\'t be reached'], 'redacted' => ['label' => 'Secrets kept out', 'icon' => 'fa-user-secret', 'where' => "ai_usage_redacted > 0", 'title' => 'Passwords or keys were taken out before they were sent'], 'test' => ['label' => 'Tests', 'icon' => 'fa-vial', 'where' => "ai_usage_feature = 'test'", 'title' => 'Try it, from a model\'s window'], ]; $view = is_string($_GET['view'] ?? null) && isset($views[$_GET['view']]) ? $_GET['view'] : ''; $provider_filter = intval($_GET['provider'] ?? 0); $model_filter = intval($_GET['model'] ?? 0); $user_filter = intval($_GET['user'] ?? 0); $feature_filter = is_string($_GET['feature'] ?? null) && isset($features[$_GET['feature']]) ? $_GET['feature'] : ''; $from = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $dtf) ? $dtf : '1970-01-01'; $to = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $dtt) ? $dtt : '2099-12-31'; // Everything but the view - the views are counted with it $where = ["DATE(ai_usage_created_at) BETWEEN '$from' AND '$to'"]; if ($q !== '') { $where[] = "(ai_usage_model LIKE '%$q%' OR ai_usage_provider_name LIKE '%$q%' OR ai_usage_feature LIKE '%$q%' OR ai_usage_use_case LIKE '%$q%' OR user_name LIKE '%$q%' OR client_name LIKE '%$q%' OR ai_usage_error LIKE '%$q%')"; } if ($provider_filter) { $where[] = "ai_usage_provider_id = $provider_filter"; } if ($model_filter) { $where[] = "ai_usage_model_id = $model_filter"; } if ($user_filter) { $where[] = "ai_usage_user_id = $user_filter"; } if ($feature_filter !== '') { $where[] = "ai_usage_feature = '" . escapeSql($feature_filter) . "'"; } $where_sql = implode(' AND ', $where); $from_sql = "FROM ai_usage LEFT JOIN users ON user_id = ai_usage_user_id LEFT JOIN clients ON client_id = ai_usage_client_id"; $sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS ai_usage.*, user_name, client_name $from_sql WHERE $where_sql AND {$views[$view]['where']} ORDER BY {$sortable_columns[$sort]} LIMIT $record_from, $record_to"); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); // Each view counted for the rest of the filters, and the totals for this one $count_sql = []; foreach ($views as $definition) { $count_sql[] = "COALESCE(SUM({$definition['where']}), 0)"; } $counts = array_combine(array_keys($views), array_map('intval', mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . " $from_sql WHERE $where_sql")))); $totals = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(*) AS requests, COALESCE(SUM(ai_usage_tokens_in), 0) AS tokens_in, COALESCE(SUM(ai_usage_tokens_out), 0) AS tokens_out, COALESCE(SUM(ai_usage_ok = 0), 0) AS failed, COALESCE(SUM(ai_usage_redacted), 0) AS redacted, COALESCE(AVG(IF(ai_usage_ok = 1, ai_usage_ms, NULL)), 0) AS ms, COUNT(DISTINCT ai_usage_user_id) AS people $from_sql WHERE $where_sql AND {$views[$view]['where']}")); $providers = mysqli_query($mysqli, "SELECT ai_provider_id, ai_provider_name, ai_provider_kind FROM ai_providers ORDER BY ai_provider_name"); $people = mysqli_query($mysqli, "SELECT DISTINCT user_id, user_name FROM ai_usage JOIN users ON user_id = ai_usage_user_id ORDER BY user_name"); $model_row = $model_filter ? aiModelRow($model_filter) : null; $retention = appLogRetention(); $sort_link = function ($column, $label) use ($url_query_strings_sort, $sort, $disp, $order_icon) { return '' . $label . ($sort === $column ? " $order_icon" : '') . ''; }; // A view keeps the filters, and starts from its first page $view_url = function ($key) { $query = $_GET; unset($query['page']); $query['view'] = $key; return '?' . http_build_query(array_filter($query, function ($value) { return $value !== '' && $value !== null && $value !== []; })); }; // The page without one filter $without_url = function ($key) { $query = $_GET; unset($query['page'], $query[$key]); return '?' . http_build_query($query); }; $ms_text = function ($ms) { $ms = intval($ms); return $ms >= 1000 ? number_format($ms / 1000, 1) . 's' : $ms . 'ms'; }; ?>

AI Usage

$view, 'model' => $model_filter ?: ''] as $keep => $value) { ?>
Just
Requests
Tokens in
Tokens out
Failed
Secrets kept out
Answer time
">
secret kept out
' . (intval($row['ai_usage_user_id']) ? 'Removed' : 'ITFlow') . '' ?> -
in
out
-
'AI requests', 'icon' => 'fa-chart-bar', 'views' => ['view'], 'view' => $view !== '' ? ['icon' => $views[$view]['icon'], 'title' => 'None ' . ($view === 'test' ? 'tried yet' : 'yet'), 'text' => escapeHtml($views[$view]['title']) . ' - they show here.'] : null, 'text' => 'Each time an agent rewords a reply, summarizes a ticket or drafts a document, it shows here - who, for what, the model and the tokens it took. ' . ($retention['audit'] ? 'Kept for ' . escapeHtml(appLogRetentionText($retention['audit'])) . ', as the audit log is.' : 'Kept as long as the audit log is.'), ]; ?>