From 89358f92fb1c0abd682bc212776822b5017c4e2f Mon Sep 17 00:00:00 2001 From: Stevani Andolo Date: Thu, 15 May 2025 16:34:52 +1000 Subject: [PATCH] MDL-84296 core_hub: Added detailed AI usage data to registration page --- lang/en/hub.php | 10 +++ lib/classes/hub/registration.php | 95 +++++++++++++++++++++++++++- lib/templates/ai_usage_data.mustache | 93 +++++++++++++++++++++++++++ lib/tests/hub/registration_test.php | 88 +++++++++++++++++++------- 4 files changed, 262 insertions(+), 24 deletions(-) create mode 100644 lib/templates/ai_usage_data.mustache diff --git a/lang/en/hub.php b/lang/en/hub.php index 77815901af4..45384b972b3 100644 --- a/lang/en/hub.php +++ b/lang/en/hub.php @@ -24,6 +24,7 @@ */ $string['activeparticipantnumberaverage'] = 'Average number of recently active participants ({$a})'; $string['activeusersnumber'] = 'Number of recently active users ({$a})'; +$string['aiusagedata'] = 'AI usage data'; $string['aiusagestats'] = 'AI usage stats ({$a->timefrom} - {$a->timeto})'; $string['analyticsactions'] = 'Number of actions taken on generated predictions ({$a})'; $string['analyticsactionsnotuseful'] = 'Number of actions marking a prediction as not useful ({$a})'; @@ -34,6 +35,7 @@ $string['audience_help'] = 'Select the intended audience for the course.'; $string['audienceeducators'] = 'Educators'; $string['audiencestudents'] = 'Students'; $string['audienceadmins'] = 'Moodle administrators'; +$string['average_time'] = 'Average processing time ({$a})'; $string['badgesnumber'] = 'Number of badges ({$a})'; $string['communityremoved'] = 'That course link has been removed from your list'; $string['confirmregistration'] = 'Confirm registration'; @@ -66,6 +68,7 @@ $string['errorwstokenreset'] = '{$a}. Registration token on this site has been r $string['errorregistrationupdate'] = 'An error occurred during registration update ({$a})'; $string['experttipsandinsights'] = 'Expert tips and insights'; $string['experttipsandinsightsdesc'] = 'Yes, sign me up for exclusive tips, strategies, and insights from Moodle experts and Certified Partners. Update your preferences or unsubscribe any time, as covered in our Privacy Notice.'; +$string['fail_count'] = 'Fail count ({$a})'; $string['geolocation'] = 'Geolocation'; $string['geolocation_help'] = 'In future we may provide location-based searching. If you want to specify the location for your course use a latitude/longitude value here (eg: -31.947884,115.871285). One way to find this is to use Google Maps.'; $string['imageurl'] = 'Image URL'; @@ -80,6 +83,7 @@ $string['licence_link'] = 'licenses'; $string['logourl'] = 'Logo URL'; $string['mobilenotificationsenabled'] = 'Mobile notifications enabled ({$a})'; $string['mobileservicesenabled'] = 'Mobile services enabled ({$a})'; +$string['models'] = 'Models'; $string['modulenumberaverage'] = 'Average number of course modules ({$a})'; $string['mustselectsubject'] = 'You must select a subject'; $string['name'] = 'Name'; @@ -87,6 +91,7 @@ $string['name_help'] = 'This name will be showing in the course listing.'; $string['neverchecked'] = 'Never checked'; $string['next'] = 'Next'; $string['no'] = 'No'; +$string['noaiusagedata'] = 'AI usage data (none)'; $string['nocheckstatusfromunreghub'] = 'The site is not registered on the hub so the status can not be checked.'; $string['nohubselected'] = 'No hub selected'; $string['none'] = 'None'; @@ -98,6 +103,7 @@ $string['policyagreeddesc'] = 'I agree to the Pri $string['postaladdress'] = 'Postal address'; $string['postaladdress_help'] = 'Postal address of this site, or of the entity represented by this site.'; $string['postsnumber'] = 'Number of posts ({$a})'; +$string['predominant_error'] = 'Predominant HTTP error ({$a})'; $string['previousregistrationdeleted'] = 'The previous registration has been deleted from {$a}. You can restart the registration process. Thank you.'; $string['primaryauthtype'] = 'Primary authentication type ({$a})'; $string['questionsnumber'] = 'Number of questions ({$a})'; @@ -184,8 +190,12 @@ $string['siteurl_help'] = 'The URL is the address of this site. If privacy sett $string['siteversion'] = 'Moodle version'; $string['siteversion_help'] = 'The Moodle version of this site.'; $string['skipregistration'] = 'Skip'; +$string['success_count'] = 'Success count ({$a})'; $string['subject'] = 'Subject'; $string['subject_help'] = 'Select the main subject area which the course covers.'; +$string['timefrom'] = 'Time from ({$a})'; +$string['timeto'] = 'Time to ({$a})'; +$string['time_range'] = 'Time range'; $string['type'] = 'Shared'; $string['unregister'] = 'Unregister'; $string['unregistrationerror'] = 'An error occurred while attempting to unregister the site: {$a}'; diff --git a/lib/classes/hub/registration.php b/lib/classes/hub/registration.php index 42f891b0288..fd8dadd1cc7 100644 --- a/lib/classes/hub/registration.php +++ b/lib/classes/hub/registration.php @@ -239,6 +239,8 @@ class registration { * @return string */ public static function get_stats_summary($siteinfo) { + global $OUTPUT; + $fieldsneedconfirm = self::get_new_registration_fields(); $summary = html_writer::tag('p', get_string('sendfollowinginfo_help', 'hub')) . html_writer::start_tag('ul'); @@ -284,7 +286,7 @@ class registration { 'sitetheme' => get_string('sitetheme', 'hub', $siteinfo['sitetheme']), 'primaryauthtype' => get_string('primaryauthtype', 'hub', $siteinfo['primaryauthtype']), 'pluginusage' => get_string('pluginusagedata', 'hub', $pluginusagelinks), - 'aiusage' => get_string('aiusagestats', 'hub', self::get_ai_usage_time_range(true)), + 'aiusage' => $OUTPUT->render_from_template('core/ai_usage_data', ['aiusagedata' => self::show_ai_usage()]), ]; foreach ($senddata as $key => $str) { @@ -725,6 +727,97 @@ class registration { ]; } + /** + * Displays AI usage data for all providers. + * + * @return array Array containing usage data, grouped by provider + */ + public static function show_ai_usage(): array { + // Initialize aiusage collection. + $aiusage = []; + + // Process each provider's data. + foreach (self::get_ai_usage_data() as $provider => $actions) { + if ($provider === 'time_range') { + $aiusage['timerange'] = [ + 'label' => get_string($provider, 'hub'), + 'values' => self::format_ai_usage_actions($actions), + ]; + } else { + // Initialize provider data structure. + $aiusage['providers'][] = [ + 'providername' => get_string('pluginname', $provider), + 'aiactions' => self::format_ai_usage_actions($actions), + ]; + } + } + + return $aiusage; + } + + /** + * Formats individual actions for a provider. + * + * @param array $actions Raw actions data + * @return array Formatted action data + */ + private static function format_ai_usage_actions(array $actions): array { + $formattedactions = []; + + foreach ($actions as $action => $values) { + if (in_array($action, ['timefrom', 'timeto'])) { + $formattedactions[] = get_string($action, 'hub', userdate($values)); + } else { + $formattedactions[] = [ + 'actionname' => get_string("action_$action", 'core_ai'), + 'aiactionvalues' => self::format_ai_usage_action_values($values), + ]; + } + } + + return $formattedactions; + } + + /** + * Formats action values into formatted strings. + * + * @param array $values Action values to format + * @return array Formatted action values + */ + private static function format_ai_usage_action_values(array $values): array { + $formattedvalues = []; + + foreach ($values as $key => $value) { + if (get_string_manager()->string_exists($key, 'hub')) { + if ($key === 'models') { + $formattedvalues[] = [ + 'label' => get_string($key, 'hub'), + 'models' => self::format_ai_usage_model_values($value), + ]; + } else { + $formattedvalues[]['values'] = get_string($key, 'hub', $value); + } + } + } + + return $formattedvalues; + } + + /** + * Formats model values with their counts. + * + * @param array $values Formatted model values + */ + private static function format_ai_usage_model_values(array $values): array { + $modelvalues = []; + + foreach ($values as $model => $count) { + $modelvalues[] = "$model ($count[count])"; + } + + return $modelvalues; + } + /** * Get AI usage data. * diff --git a/lib/templates/ai_usage_data.mustache b/lib/templates/ai_usage_data.mustache new file mode 100644 index 00000000000..b86d50239e2 --- /dev/null +++ b/lib/templates/ai_usage_data.mustache @@ -0,0 +1,93 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/ai_usage_data + + Template for AI usage data. + + Example context (json): + { + "aiusagedata": [{ + "providers": [{ + "providerName": "AI provider name", + "aiactions": [{ + "actionName": "AI action name", + "aiactionvalues": [{ + "success_count": "1", + "fail_count": "2", + "average_time": "15", + "predominant_error": "403", + "models": [{ + "gpt-4o": "1" + }] + }] + }] + }], + "timerange": [{ + "label": "Time range for stats", + "values": [{ + "timefrom": "Time from: Friday, 2 May 2025, 1:22 AM", + "timeto": "Time from: Friday, 3 May 2025, 1:22 AM" + }] + }] + }] + } +}} + +{{#aiusagedata}} + {{#str}} aiusagedata, hub {{/str}}: +
    + {{! AI providers. }} + {{#providers}} +
  • {{providername}}:
  • + {{#aiactions}} +
      +
    • {{actionname}}:
    • + {{#aiactionvalues}} +
        + {{#values}} +
      • {{values}}
      • + {{/values}} + + {{^values}} +
      • {{label}}:
      • + {{#models}} +
          +
        • {{.}}
        • +
        + {{/models}} + {{/values}} +
      + {{/aiactionvalues}} +
    + {{/aiactions}} + {{/providers}} + + {{! Time range for stats. }} + {{#timerange}} +
  • {{label}}:
  • + {{#values}} +
      +
    • {{.}}
    • +
    + {{/values}} + {{/timerange}} +
+{{/aiusagedata}} +{{^aiusagedata}} + {{#str}} noaiusagedata, hub {{/str}} +{{/aiusagedata}} diff --git a/lib/tests/hub/registration_test.php b/lib/tests/hub/registration_test.php index a76b725ad0f..22956fe2fef 100644 --- a/lib/tests/hub/registration_test.php +++ b/lib/tests/hub/registration_test.php @@ -90,16 +90,44 @@ final class registration_test extends \advanced_testcase { * Test the AI usage data is calculated correctly. */ public function test_get_ai_usage(): void { - global $CFG, $DB; $this->resetAfterTest(); - $clock = $this->mock_clock_with_frozen(); + $clock = $this->mock_clock_with_frozen(1700000000); + $this->generate_ai_usage_data(); + + // Get our site info and check the expected calculations are correct. + $siteinfo = registration::get_site_info(); + $aisuage = json_decode($siteinfo['aiusage']); + // Check generated text. + $this->assertEquals(1, $aisuage->aiprovider_openai->generate_text->success_count); + $this->assertEquals(0, $aisuage->aiprovider_openai->generate_text->fail_count); + // Check generated images. + $this->assertEquals(2, $aisuage->aiprovider_openai->generate_image->success_count); + $this->assertEquals(3, $aisuage->aiprovider_openai->generate_image->fail_count); + $this->assertEquals(15, $aisuage->aiprovider_openai->generate_image->average_time); + $this->assertEquals(403, $aisuage->aiprovider_openai->generate_image->predominant_error); + // Check time range is set correctly. + $this->assertEquals($clock->time() - WEEKSECS, $aisuage->time_range->timefrom); + $this->assertEquals($clock->time(), $aisuage->time_range->timeto); + // Check model counts. $gpt4omodel = 'gpt-4o'; $dalle3model = 'dall-e-3'; + $this->assertEquals(1, $aisuage->aiprovider_openai->generate_text->models->{$gpt4omodel}->count); + $this->assertEquals(2, $aisuage->aiprovider_openai->generate_image->models->{$dalle3model}->count); + $this->assertEquals(3, $aisuage->aiprovider_openai->generate_image->models->unknown->count); + } + + /** + * Create some dummy AI usage data. + */ + private function generate_ai_usage_data(): void { + global $DB; + + $clock = $this->mock_clock_with_frozen(1700000000); // Record some generated text. $record = new \stdClass(); - $record->provider = 'openai'; + $record->provider = 'aiprovider_openai'; $record->actionname = 'generate_text'; $record->actionid = 1; $record->userid = 1; @@ -107,14 +135,14 @@ final class registration_test extends \advanced_testcase { $record->success = true; $record->timecreated = $clock->time() - 5; $record->timecompleted = $clock->time(); - $record->model = $gpt4omodel; + $record->model = 'gpt-4o'; $DB->insert_record('ai_action_register', $record); // Record a generated image. $record->actionname = 'generate_image'; $record->actionid = 111; $record->timecreated = $clock->time() - 20; - $record->model = $dalle3model; + $record->model = 'dall-e-3'; $DB->insert_record('ai_action_register', $record); // Record another image. $record->actionid = 222; @@ -134,24 +162,38 @@ final class registration_test extends \advanced_testcase { $record->actionid = 6; $record->errorcode = 404; $DB->insert_record('ai_action_register', $record); + } - // Get our site info and check the expected calculations are correct. - $siteinfo = registration::get_site_info(); - $aisuage = json_decode($siteinfo['aiusage']); - // Check generated text. - $this->assertEquals(1, $aisuage->openai->generate_text->success_count); - $this->assertEquals(0, $aisuage->openai->generate_text->fail_count); - // Check generated images. - $this->assertEquals(2, $aisuage->openai->generate_image->success_count); - $this->assertEquals(3, $aisuage->openai->generate_image->fail_count); - $this->assertEquals(15, $aisuage->openai->generate_image->average_time); - $this->assertEquals(403, $aisuage->openai->generate_image->predominant_error); - // Check time range is set correctly. - $this->assertEquals($clock->time() - WEEKSECS, $aisuage->time_range->timefrom); - $this->assertEquals($clock->time(), $aisuage->time_range->timeto); - // Check model counts. - $this->assertEquals(1, $aisuage->openai->generate_text->models->{$gpt4omodel}->count); - $this->assertEquals(2, $aisuage->openai->generate_image->models->{$dalle3model}->count); - $this->assertEquals(3, $aisuage->openai->generate_image->models->unknown->count); + /** + * Test the show AI usage data. + */ + public function test_show_ai_usage(): void { + $this->resetAfterTest(); + + // Init the registration class. + $registration = new registration(); + + // There should be no data to show yet. + $aisuagedata = $registration->show_ai_usage(); + $this->assertTrue(empty($aisuagedata)); + + // After generating some data, there should now be some data to show. + $this->generate_ai_usage_data(); + $aisuagedata = $registration->show_ai_usage(); + $this->assertTrue(!empty($aisuagedata)); + + foreach ($aisuagedata['providers'] as $provider) { + $this->assertEquals('OpenAI API provider', $provider['providername']); + $this->assertTrue(!empty($provider['aiactions'])); + + foreach ($provider['aiactions'] as $action) { + $actionname = $action['actionname']; + $this->assertTrue(!empty($actionname)); + } + } + + $timerange = $aisuagedata['timerange']; + $this->assertEquals(get_string('time_range', 'hub'), $timerange['label']); + $this->assertTrue(!empty($timerange['values'])); } }