0, 'today' => 0, 'completed' => 0, 'fallback' => 0, 'failed' => 0, 'unknown' => 0, 'openai' => 0, 'gemini' => 0, 'native' => 0, 'days' => array() ); $today = new \DateTimeImmutable( 'today', wp_timezone() ); for ( $i = 6; $i >= 0; --$i ) { $result['days'][ $today->modify( '-' . $i . ' days' )->format( 'Y-m-d' ) ] = 0; } $last_id = 0; do { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- No core API reads these custom conversation records; bounded pages keep admin metrics current after writes and retention cleanup. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT id, messages, question, answer, provider, status, created_at, updated_at FROM %i WHERE id > %d ORDER BY id LIMIT 100', \AISupportAssistant\Database\DatabaseManager::get_chat_history_table(), $last_id ), ARRAY_A ); foreach ( $rows as $row ) { $last_id = (int) $row['id']; foreach ( self::turns( $row ) as $turn ) { ++$result['total']; $outcome = $turn['outcome']; ++$result[ $outcome ]; $day = substr( $turn['time'], 0, 10 ); if ( $day === current_time( 'Y-m-d' ) ) { ++$result['today']; } if ( isset( $result['days'][ $day ] ) ) { ++$result['days'][ $day ]; } // Do not attribute documentation-only fallback replies to a chat API. if ( 'fallback' === $outcome ) { continue; } $provider = $turn['provider']; $key = 'wp_native_ai' === $provider ? 'native' : ( false !== strpos( $provider, 'openai' ) ? 'openai' : ( preg_match( '/gemini|google/', $provider ) ? 'gemini' : '' ) ); if ( $key ) { ++$result[ $key ]; } } } } while ( count( $rows ) === 100 ); return $result; } /** Legacy replies have unknown outcomes; do not invent historical resolution rates. */ public static function turns( array $row ): array { $messages = json_decode( $row['messages'] ?? '', true ); if ( ! is_array( $messages ) || ! $messages ) { $messages = array( array( 'role' => 'assistant', 'time' => $row['updated_at'] ?: $row['created_at'], 'outcome' => 'failed' === $row['status'] ? 'failed' : 'unknown', 'provider' => $row['provider'] ) ); } $turns = array(); foreach ( $messages as $message ) { if ( 'assistant' !== ( $message['role'] ?? '' ) && 'failed' !== ( $message['outcome'] ?? '' ) ) { continue; } $outcome = $message['outcome'] ?? 'unknown'; $turns[] = array( 'outcome' => in_array( $outcome, array( 'completed', 'fallback', 'failed' ), true ) ? $outcome : 'unknown', 'time' => $message['time'] ?? $row['created_at'], 'provider' => $message['provider'] ?? $row['provider'] ); } // Old failed records have an unanswered user message without outcome metadata. $last = end( $messages ); if ( 'failed' === $row['status'] && 'user' === ( $last['role'] ?? '' ) && ! isset( $last['outcome'] ) ) { $turns[] = array( 'outcome' => 'failed', 'time' => $last['time'] ?? $row['updated_at'], 'provider' => $row['provider'] ); } return $turns; } }