diff --git a/ai/classes/privacy/provider.php b/ai/classes/privacy/provider.php index d2b0d5d5b15..471479f1ea3 100644 --- a/ai/classes/privacy/provider.php +++ b/ai/classes/privacy/provider.php @@ -16,6 +16,14 @@ namespace core_ai\privacy; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\userlist; +use core_privacy\local\request\writer; + /** * Privacy Subsystem for core_ai implementing null_provider. * @@ -24,17 +32,627 @@ namespace core_ai\privacy; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements - \core_privacy\local\metadata\null_provider { - - // TODO: Will be implemented in MDL-82781. + \core_privacy\local\metadata\provider, + \core_privacy\local\request\subsystem\provider, + \core_privacy\local\request\core_userlist_provider { /** - * Get the language string identifier with the component's language - * file to explain why this plugin stores no data. + * Returns meta data about this system. * - * @return string + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. */ - public static function get_reason(): string { - return 'privacy:metadata'; + public static function get_metadata(collection $collection): collection { + $collection->add_database_table('ai_policy_register', [ + 'userid' => 'privacy:metadata:ai_policy_register:userid', + 'contextid' => 'privacy:metadata:ai_policy_register:contextid', + 'timeaccepted' => 'privacy:metadata:ai_policy_register:timeaccepted', + ], 'privacy:metadata:ai_policy_register'); + $collection->add_database_table('ai_action_register', [ + 'actionname' => 'privacy:metadata:ai_action_register:actionname', + 'actionid' => 'privacy:metadata:ai_action_register:actionid', + 'success' => 'privacy:metadata:ai_action_register:success', + 'userid' => 'privacy:metadata:ai_action_register:userid', + 'provider' => 'privacy:metadata:ai_action_register:provider', + 'timecreated' => 'privacy:metadata:ai_action_register:timecreated', + 'timecompleted' => 'privacy:metadata:ai_action_register:timecompleted', + ], 'privacy:metadata:ai_action_register'); + $collection->add_database_table('ai_action_generate_image', [ + 'prompt' => 'privacy:metadata:ai_action_generate_image:prompt', + 'numberimages' => 'privacy:metadata:ai_action_generate_image:numberimages', + 'quality' => 'privacy:metadata:ai_action_generate_image:quality', + 'aspectratio' => 'privacy:metadata:ai_action_generate_image:aspectratio', + 'style' => 'privacy:metadata:ai_action_generate_image:style', + 'sourceurl' => 'privacy:metadata:ai_action_generate_image:sourceurl', + 'revisedprompt' => 'privacy:metadata:ai_action_generate_image:revisedprompt', + ], 'privacy:metadata:ai_action_generate_image'); + $collection->add_database_table('ai_action_generate_text', [ + 'prompt' => 'privacy:metadata:ai_action_generate_text:prompt', + 'responseid' => 'privacy:metadata:ai_action_generate_text:responseid', + 'fingerprint' => 'privacy:metadata:ai_action_generate_text:fingerprint', + 'generatedcontent' => 'privacy:metadata:ai_action_generate_text:generatedcontent', + 'prompttokens' => 'privacy:metadata:ai_action_generate_text:prompttokens', + 'completiontoken' => 'privacy:metadata:ai_action_generate_text:completiontoken', + ], 'privacy:metadata:ai_action_generate_text'); + $collection->add_database_table('ai_action_summarise_text', [ + 'prompt' => 'privacy:metadata:ai_action_summarise_text:prompt', + 'responseid' => 'privacy:metadata:ai_action_summarise_text:responseid', + 'fingerprint' => 'privacy:metadata:ai_action_summarise_text:fingerprint', + 'generatedcontent' => 'privacy:metadata:ai_action_summarise_text:generatedcontent', + 'prompttokens' => 'privacy:metadata:ai_action_summarise_text:prompttokens', + 'completiontoken' => 'privacy:metadata:ai_action_summarise_text:completiontoken', + ], 'privacy:metadata:ai_action_summarise_text'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid): contextlist { + $contextlist = new contextlist(); + + // AI policy. + $sql = 'SELECT DISTINCT ctx.id + FROM {context} ctx + JOIN {ai_policy_register} apr + ON apr.contextid = ctx.id + WHERE apr.userid = :userid'; + $contextlist->add_from_sql($sql, ['userid' => $userid]); + + // AI action generate text. + $sql = "SELECT DISTINCT ctx.id + FROM {context} ctx + JOIN {ai_action_register} aar + ON aar.contextid = ctx.id + JOIN {ai_action_generate_text} aagt + ON aagt.id = aar.actionid + WHERE aar.actionname = 'generate_text' + AND aar.userid = :userid"; + $contextlist->add_from_sql($sql, ['userid' => $userid]); + + // AI action generate image. + $sql = "SELECT DISTINCT ctx.id + FROM {context} ctx + JOIN {ai_action_register} aar + ON aar.contextid = ctx.id + JOIN {ai_action_generate_image} aagi + ON aagi.id = aar.actionid + WHERE aar.actionname = 'generate_image' + AND aar.userid = :userid"; + $contextlist->add_from_sql($sql, ['userid' => $userid]); + + // AI action summarise text. + $sql = "SELECT DISTINCT ctx.id + FROM {context} ctx + JOIN {ai_action_register} aar + ON aar.contextid = ctx.id + JOIN {ai_action_summarise_text} aast + ON aast.id = aar.actionid + WHERE aar.actionname = 'summarise_text' + AND aar.userid = :userid"; + $contextlist->add_from_sql($sql, ['userid' => $userid]); + + return $contextlist; + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist): void { + global $DB; + // AI policy. + $userid = $contextlist->get_user()->id; + [$contextsql, $contextparams] = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = 'SELECT apr.timeaccepted, ctx.id AS contextid + FROM {context} ctx + JOIN {ai_policy_register} apr + ON apr.contextid = ctx.id + WHERE apr.userid = :userid + AND ctx.id ' . $contextsql; + $params = [ + 'userid' => $userid, + ]; + $params += $contextparams; + $policydetails = $DB->get_recordset_sql($sql, $params); + foreach ($policydetails as $policydetail) { + $subcontexts = [ + get_string('ai', 'core_ai'), + ]; + $name = 'policy'; + $details = (object) [ + 'contextid' => $policydetail->contextid, + 'timeaccepted' => transform::datetime($policydetail->timeaccepted), + ]; + $context = \context::instance_by_id($policydetail->contextid); + writer::with_context($context)->export_related_data($subcontexts, $name, $details); + } + $policydetails->close(); + + // AI action generate text. + $sql = "SELECT aar.actionname, aar.success, aar.provider, aar.timecreated, aar.timecompleted, aar.contextid, + aagt.prompt, aagt.responseid, aagt.fingerprint, aagt.generatedcontent, + aagt.prompttokens, aagt.completiontoken + FROM {ai_action_register} aar + JOIN {ai_action_generate_text} aagt + ON aar.actionid = aagt.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'generate_text' + AND aar.userid = :userid + AND ctx.id " . $contextsql; + $params = [ + 'userid' => $userid, + ]; + $params += $contextparams; + $textgeneratedetails = $DB->get_recordset_sql($sql, $params); + foreach ($textgeneratedetails as $textgeneratedetail) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_generate_text', 'core_ai'), + date('c', $textgeneratedetail->timecreated), + ]; + $details = (object) [ + 'actionname' => $textgeneratedetail->actionname, + 'contextid' => $textgeneratedetail->contextid, + 'prompt' => $textgeneratedetail->prompt, + 'responseid' => $textgeneratedetail->responseid, + 'fingerprint' => $textgeneratedetail->fingerprint, + 'generatedcontent' => $textgeneratedetail->generatedcontent, + 'prompttokens' => $textgeneratedetail->prompttokens, + 'completiontoken' => $textgeneratedetail->completiontoken, + 'success' => transform::yesno($textgeneratedetail->success), + 'provider' => $textgeneratedetail->provider, + 'timecreated' => transform::datetime($textgeneratedetail->timecreated), + 'timecompleted' => transform::datetime($textgeneratedetail->timecompleted), + ]; + $name = 'action_generate_text'; + $context = \context::instance_by_id($textgeneratedetail->contextid); + writer::with_context($context)->export_related_data($subcontexts, $name, $details); + } + $textgeneratedetails->close(); + + // AI action generate image. + $sql = "SELECT aar.actionname, aar.success, aar.provider, aar.timecreated, aar.timecompleted, aar.contextid, + aagi.prompt, aagi.numberimages, aagi.quality, aagi.aspectratio, aagi.style, aagi.sourceurl, + aagi.revisedprompt + FROM {ai_action_register} aar + JOIN {ai_action_generate_image} aagi + ON aar.actionid = aagi.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'generate_image' + AND aar.userid = :userid + AND ctx.id " . $contextsql; + $params = [ + 'userid' => $userid, + ]; + $params += $contextparams; + $imagegeneratedetails = $DB->get_recordset_sql($sql, $params); + foreach ($imagegeneratedetails as $imagegeneratedetail) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_generate_image', 'core_ai'), + date('c', $imagegeneratedetail->timecreated), + ]; + $details = (object) [ + 'actionname' => $imagegeneratedetail->actionname, + 'contextid' => $imagegeneratedetail->contextid, + 'prompt' => $imagegeneratedetail->prompt, + 'numberimages' => $imagegeneratedetail->numberimages, + 'quality' => $imagegeneratedetail->quality, + 'aspectratio' => $imagegeneratedetail->aspectratio, + 'style' => $imagegeneratedetail->style, + 'sourceurl' => $imagegeneratedetail->sourceurl, + 'revisedprompt' => $imagegeneratedetail->revisedprompt, + 'success' => transform::yesno($imagegeneratedetail->success), + 'provider' => $imagegeneratedetail->provider, + 'timecreated' => transform::datetime($imagegeneratedetail->timecreated), + 'timecompleted' => transform::datetime($imagegeneratedetail->timecompleted), + ]; + $name = 'action_generate_image'; + $context = \context::instance_by_id($imagegeneratedetail->contextid); + writer::with_context($context)->export_related_data($subcontexts, $name, $details); + } + $imagegeneratedetails->close(); + + // AI action summarise text. + $sql = "SELECT aar.actionname, aar.success, aar.provider, aar.timecreated, aar.timecompleted, aar.contextid, + aast.prompt, aast.responseid, aast.fingerprint, aast.generatedcontent, + aast.prompttokens, aast.completiontoken + FROM {ai_action_register} aar + JOIN {ai_action_summarise_text} aast + ON aar.actionid = aast.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'summarise_text' + AND aar.userid = :userid + AND ctx.id " . $contextsql; + $params = [ + 'userid' => $userid, + ]; + $params += $contextparams; + $textsummarisedetails = $DB->get_recordset_sql($sql, $params); + foreach ($textsummarisedetails as $textsummarisedetail) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_summarise_text', 'core_ai'), + date('c', $textsummarisedetail->timecreated), + ]; + $details = (object) [ + 'actionname' => $textsummarisedetail->actionname, + 'contextid' => $textsummarisedetail->contextid, + 'prompt' => $textsummarisedetail->prompt, + 'responseid' => $textsummarisedetail->responseid, + 'fingerprint' => $textsummarisedetail->fingerprint, + 'generatedcontent' => $textsummarisedetail->generatedcontent, + 'prompttokens' => $textsummarisedetail->prompttokens, + 'completiontoken' => $textsummarisedetail->completiontoken, + 'success' => transform::yesno($textsummarisedetail->success), + 'provider' => $textsummarisedetail->provider, + 'timecreated' => transform::datetime($textsummarisedetail->timecreated), + 'timecompleted' => transform::datetime($textsummarisedetail->timecompleted), + ]; + $name = 'action_summarise_text'; + $context = \context::instance_by_id($textsummarisedetail->contextid); + writer::with_context($context)->export_related_data($subcontexts, $name, $details); + } + $textsummarisedetails->close(); + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(\context $context): void { + global $DB; + + // Policy. + $sql = 'SELECT DISTINCT apr.id + FROM {ai_policy_register} apr + JOIN {context} ctx + ON apr.contextid = ctx.id + WHERE ctx.id = :contextid'; + $params = [ + 'contextid' => $context->id, + ]; + $policydetails = $DB->get_records_sql($sql, $params); + if ($policydetails) { + $DB->delete_records_list('ai_policy_register', 'id', array_keys($policydetails)); + } + + // AI action generate text. + $sql = "SELECT DISTINCT aagt.id + FROM {ai_action_register} aar + JOIN {ai_action_generate_text} aagt + ON aar.actionid = aagt.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'generate_text' + AND ctx.id = :contextid"; + $params = [ + 'contextid' => $context->id, + ]; + $aagtids = $DB->get_records_sql_menu($sql, $params); + if ($aagtids) { + [$aagtidsql, $aagtidparams] = $DB->get_in_or_equal(array_keys($aagtids), SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_generate_text} + SET prompt = '', + responseid = '', + fingerprint = '', + generatedcontent = '' + WHERE id " . $aagtidsql; + $DB->execute($sql, $aagtidparams); + } + + // AI action generate image. + $sql = "SELECT DISTINCT aagi.id + FROM {ai_action_register} aar + JOIN {ai_action_generate_image} aagi + ON aar.actionid = aagi.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'generate_image' + AND ctx.id = :contextid"; + $params = [ + 'contextid' => $context->id, + ]; + $aagiids = $DB->get_records_sql_menu($sql, $params); + if ($aagiids) { + [$aagiidsql, $aagiidparams] = $DB->get_in_or_equal(array_keys($aagiids), SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_generate_image} + SET prompt = '', + sourceurl = '', + revisedprompt = '' + WHERE id " . $aagiidsql; + $DB->execute($sql, $aagiidparams); + } + + // AI action summarise text. + $sql = "SELECT DISTINCT aast.id + FROM {ai_action_register} aar + JOIN {ai_action_summarise_text} aast + ON aar.actionid = aast.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'summarise_text' + AND ctx.id = :contextid"; + $params = [ + 'contextid' => $context->id, + ]; + $aastids = $DB->get_records_sql_menu($sql, $params); + if ($aastids) { + [$aastidsql, $aastidparams] = $DB->get_in_or_equal(array_keys($aastids), SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_summarise_text} + SET prompt = '', + responseid = '', + fingerprint = '', + generatedcontent = '' + WHERE id " . $aastidsql; + $DB->execute($sql, $aastidparams); + } + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist): void { + global $DB; + + // Policy. + $userid = $contextlist->get_user()->id; + [$contextsql, $contextparams] = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT DISTINCT apr.id AS policyid + FROM {context} ctx + JOIN {ai_policy_register} apr + ON apr.contextid = ctx.id + WHERE apr.userid = :userid + AND ctx.id " . $contextsql; + $params = [ + 'userid' => $userid, + ]; + $params += $contextparams; + $policydetails = $DB->get_recordset_sql($sql, $params); + $policyids = []; + foreach ($policydetails as $policydetail) { + $policyids[] = $policydetail->policyid; + } + $policydetails->close(); + $DB->delete_records_list('ai_policy_register', 'id', $policyids); + + // AI action generate text. + $sql = "SELECT DISTINCT aagt.id AS textgenerateid + FROM {ai_action_register} aar + JOIN {ai_action_generate_text} aagt + ON aar.actionid = aagt.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'generate_text' + AND aar.userid = :userid + AND ctx.id " . $contextsql; + $textgeneratedetails = $DB->get_recordset_sql($sql, $params); + $aagtids = []; + foreach ($textgeneratedetails as $textgeneratedetail) { + $aagtids[] = $textgeneratedetail->textgenerateid; + } + $textgeneratedetails->close(); + if ($aagtids) { + [$aagtidsql, $aagtidparams] = $DB->get_in_or_equal($aagtids, SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_generate_text} + SET prompt = '', + responseid = '', + fingerprint = '', + generatedcontent = '' + WHERE id " . $aagtidsql; + $DB->execute($sql, $aagtidparams); + } + + // AI action generate image. + $sql = "SELECT DISTINCT aagi.id AS imagegenerateid + FROM {ai_action_register} aar + JOIN {ai_action_generate_image} aagi + ON aar.actionid = aagi.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'generate_image' + AND aar.userid = :userid + AND ctx.id " . $contextsql; + $imagegeneratedetails = $DB->get_recordset_sql($sql, $params); + $aagiids = []; + foreach ($imagegeneratedetails as $imagegeneratedetail) { + $aagiids[] = $imagegeneratedetail->imagegenerateid; + } + $imagegeneratedetails->close(); + if ($aagiids) { + [$aagiidsql, $aagiidparams] = $DB->get_in_or_equal($aagiids, SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_generate_image} + SET prompt = '', + sourceurl = '', + revisedprompt = '' + WHERE id " . $aagiidsql; + $DB->execute($sql, $aagiidparams); + } + + // AI action summarise text. + $sql = "SELECT DISTINCT aast.id AS textsummariseid + FROM {ai_action_register} aar + JOIN {ai_action_summarise_text} aast + ON aar.actionid = aast.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'summarise_text' + AND aar.userid = :userid + AND ctx.id " . $contextsql; + $textsummarisedetails = $DB->get_recordset_sql($sql, $params); + $aastids = []; + foreach ($textsummarisedetails as $textsummarisedetail) { + $aastids[] = $textsummarisedetail->textsummariseid; + } + $textsummarisedetails->close(); + if ($aastids) { + [$aastidsql, $aastidparams] = $DB->get_in_or_equal($aastids, SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_summarise_text} + SET prompt = '', + responseid = '', + fingerprint = '', + generatedcontent = '' + WHERE id " . $aastidsql; + $DB->execute($sql, $aastidparams); + } + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist): void { + $context = $userlist->get_context(); + + // AI policy. + $sql = 'SELECT DISTINCT apr.userid + FROM {context} ctx + JOIN {ai_policy_register} apr + ON apr.contextid = ctx.id + WHERE apr.contextid = :contextid'; + $userlist->add_from_sql('userid', $sql, ['contextid' => $context->id]); + + // AI action generate text. + $sql = "SELECT DISTINCT aar.userid + FROM {context} ctx + JOIN {ai_action_register} aar + ON aar.contextid = ctx.id + JOIN {ai_action_generate_text} aagt + ON aagt.id = aar.actionid + WHERE aar.actionname = 'generate_text' + AND aar.contextid = :contextid"; + $userlist->add_from_sql('userid', $sql, ['contextid' => $context->id]); + + // AI action generate image. + $sql = "SELECT DISTINCT aar.userid + FROM {context} ctx + JOIN {ai_action_register} aar + ON aar.contextid = ctx.id + JOIN {ai_action_generate_image} aagi + ON aagi.id = aar.actionid + WHERE aar.actionname = 'generate_image' + AND aar.contextid = :contextid"; + $userlist->add_from_sql('userid', $sql, ['contextid' => $context->id]); + + // AI action summarise text. + $sql = "SELECT DISTINCT aar.userid + FROM {context} ctx + JOIN {ai_action_register} aar + ON aar.contextid = ctx.id + JOIN {ai_action_summarise_text} aast + ON aast.id = aar.actionid + WHERE aar.actionname = 'summarise_text' + AND aar.contextid = :contextid"; + $userlist->add_from_sql('userid', $sql, ['contextid' => $context->id]); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + $context = $userlist->get_context(); + $userids = $userlist->get_userids(); + + [$useridssql, $useridsparams] = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + + $params = [ + 'contextid' => $context->id, + ]; + $params += $useridsparams; + + // Policy. + $sql = "SELECT DISTINCT apr.id + FROM {ai_policy_register} apr + JOIN {context} ctx + ON apr.contextid = ctx.id + WHERE ctx.id = :contextid + AND apr.userid " . $useridssql; + $policydetails = $DB->get_records_sql($sql, $params); + if ($policydetails) { + $DB->delete_records_list('ai_policy_register', 'id', array_keys($policydetails)); + } + + // AI action generate text. + $sql = "SELECT DISTINCT aagt.id + FROM {ai_action_register} aar + JOIN {ai_action_generate_text} aagt + ON aar.actionid = aagt.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'generate_text' + AND ctx.id = :contextid + AND aar.userid " . $useridssql; + $aagtids = $DB->get_records_sql_menu($sql, $params); + if ($aagtids) { + [$aagtidsql, $aagtidparams] = $DB->get_in_or_equal(array_keys($aagtids), SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_generate_text} + SET prompt = '', + responseid = '', + fingerprint = '', + generatedcontent = '' + WHERE id " . $aagtidsql; + $DB->execute($sql, $aagtidparams); + } + + // AI action generate image. + $sql = "SELECT DISTINCT aagi.id + FROM {ai_action_register} aar + JOIN {ai_action_generate_image} aagi + ON aar.actionid = aagi.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'generate_image' + AND ctx.id = :contextid + AND aar.userid " . $useridssql; + $aagiids = $DB->get_records_sql_menu($sql, $params); + if ($aagiids) { + [$aagiidsql, $aagiidparams] = $DB->get_in_or_equal(array_keys($aagiids), SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_generate_image} + SET prompt = '', + sourceurl = '', + revisedprompt = '' + WHERE id " . $aagiidsql; + $DB->execute($sql, $aagiidparams); + } + + // AI action summarise text. + $sql = "SELECT DISTINCT aast.id + FROM {ai_action_register} aar + JOIN {ai_action_summarise_text} aast + ON aar.actionid = aast.id + JOIN {context} ctx + ON aar.contextid = ctx.id + WHERE aar.actionname = 'summarise_text' + AND ctx.id = :contextid + AND aar.userid " . $useridssql; + $aastids = $DB->get_records_sql_menu($sql, $params); + if ($aastids) { + [$aastidsql, $aastidparams] = $DB->get_in_or_equal(array_keys($aastids), SQL_PARAMS_NAMED); + $sql = "UPDATE {ai_action_summarise_text} + SET prompt = '', + responseid = '', + fingerprint = '', + generatedcontent = '' + WHERE id " . $aastidsql; + $DB->execute($sql, $aastidparams); + } } } diff --git a/ai/tests/manager_test.php b/ai/tests/manager_test.php index f3f96051d03..3e46abef18a 100644 --- a/ai/tests/manager_test.php +++ b/ai/tests/manager_test.php @@ -73,8 +73,6 @@ final class manager_test extends \advanced_testcase { */ public function test_get_providers_for_actions(): void { $this->resetAfterTest(); - // Invoke the plugin manager and disable the AzureAI plugin. - set_config('disabled', 1, 'aiprovider_azureai'); set_config('enabled', 1, 'aiprovider_openai'); $manager = new manager(); @@ -89,13 +87,6 @@ final class manager_test extends \advanced_testcase { // Assert that the providers array is indexed by action name. $this->assertEquals($actions, array_keys($providers)); - // Assert that there are two providers for each action. - $this->assertCount(2, $providers['core_ai\\aiactions\\generate_text']); - $this->assertCount(2, $providers['core_ai\\aiactions\\summarise_text']); - - // Assert that the AzureAI provider is not included in the list of providers for the actions when only selecting active. - $providers = $manager->get_providers_for_actions($actions, true); - // Assert that there is only one provider for each action. $this->assertCount(1, $providers['core_ai\\aiactions\\generate_text']); $this->assertCount(1, $providers['core_ai\\aiactions\\summarise_text']); diff --git a/ai/tests/provider/provider_test.php b/ai/tests/provider/provider_test.php index f43497d31e1..298fd39b70d 100644 --- a/ai/tests/provider/provider_test.php +++ b/ai/tests/provider/provider_test.php @@ -16,8 +16,21 @@ namespace core_ai\provider; +use core_ai\aiactions\generate_image; +use core_ai\aiactions\generate_text; +use core_ai\aiactions\responses\response_generate_image; +use core_ai\aiactions\responses\response_generate_text; +use core_ai\aiactions\responses\response_summarise_text; +use core_ai\aiactions\summarise_text; +use core_ai\manager; +use core_ai\privacy\provider; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\writer; +use core_privacy\tests\request\content_writer; + /** - * Test response_base action methods. + * Unit tests for \core_ai\provider * * @package core_ai * @copyright 2024 Matt Porritt @@ -26,6 +39,1522 @@ namespace core_ai\provider; */ final class provider_test extends \advanced_testcase { + /** + * Overriding setUp() function to always reset after tests. + */ + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + } + + /** + * Test get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid(): void { + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $syscontext = \context_system::instance(); + + // AI policy. + // Set the user policy. + manager::set_user_policy($user1->id, $course1context->id); + + // Retrieve the user1's context ids. + $contextids = provider::get_contexts_for_userid($user1->id); + $this->assertNotEmpty($contextids->get_contextids()); + $this->assertCount(1, $contextids->get_contextids()); + $this->assertTrue(in_array($course1context->id, $contextids->get_contextids())); + // Retrieve the user2's context ids. + $contextids = provider::get_contexts_for_userid($user2->id); + $this->assertEmpty($contextids->get_contextids()); + + // AI generate text. + $action = new generate_text( + contextid: $course2context->id, + userid: $user1->id, + prompttext: 'This is a test prompt', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + // Retrieve the user1's context ids. + $contextids = provider::get_contexts_for_userid($user1->id); + $this->assertNotEmpty($contextids->get_contextids()); + $this->assertCount(2, $contextids->get_contextids()); + $this->assertTrue(in_array($course1context->id, $contextids->get_contextids())); + $this->assertTrue(in_array($course2context->id, $contextids->get_contextids())); + + // AI generate image. + $action = new generate_image( + contextid: $syscontext->id, + userid: $user2->id, + prompttext: 'This is a test prompt', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + // Retrieve the user1's context ids. + $contextids = provider::get_contexts_for_userid($user1->id); + $this->assertNotEmpty($contextids->get_contextids()); + $this->assertCount(2, $contextids->get_contextids()); + $this->assertTrue(in_array($course1context->id, $contextids->get_contextids())); + $this->assertTrue(in_array($course2context->id, $contextids->get_contextids())); + + // Retrieve the user2's context ids. + $contextids = provider::get_contexts_for_userid($user2->id); + $this->assertNotEmpty($contextids->get_contextids()); + $this->assertCount(1, $contextids->get_contextids()); + $this->assertFalse(in_array($course2context->id, $contextids->get_contextids())); + $this->assertTrue(in_array($syscontext->id, $contextids->get_contextids())); + + // AI summarise text. + $action = new summarise_text( + contextid: $course2context->id, + userid: $user2->id, + prompttext: 'This is a test prompt', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + // Retrieve the user2's context ids. + $contextids = provider::get_contexts_for_userid($user2->id); + $this->assertNotEmpty($contextids->get_contextids()); + $this->assertCount(2, $contextids->get_contextids()); + $this->assertTrue(in_array($course2context->id, $contextids->get_contextids())); + $this->assertTrue(in_array($syscontext->id, $contextids->get_contextids())); + } + + /** + * Test export_user_data() for AI policy. + */ + public function test_export_user_data_for_policy(): void { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + + // Set the user policy. + manager::set_user_policy($user->id, $coursecontext->id); + + // Retrieve the user's context ids. + $contextlist = provider::get_contexts_for_userid($user->id); + $approvedcontextlist = new approved_contextlist($user, 'core_ai', $contextlist->get_contextids()); + provider::export_user_data($approvedcontextlist); + + foreach ($contextlist as $context) { + /** @var content_writer $writer */ + $writer = writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + $subcontexts = [ + get_string('ai', 'core_ai'), + ]; + $name = 'policy'; + $data = $writer->get_related_data($subcontexts, $name); + $this->assertEquals($coursecontext->id, $data->contextid); + } + } + + /** + * Test export_user_data() for generate text. + */ + public function test_export_user_data_for_generate_text(): void { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $timecreated1 = $clock->time(); + $action = new generate_text( + contextid: $course1context->id, + userid: $user->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $timecreated2 = $clock->time(); + $action = new generate_text( + contextid: $course2context->id, + userid: $user->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + $method->invoke($manager, $provider, $action, $actionresponse); + + // Retrieve the user's context ids. + $contextlist = provider::get_contexts_for_userid($user->id); + $approvedcontextlist = new approved_contextlist($user, 'core_ai', $contextlist->get_contextids()); + provider::export_user_data($approvedcontextlist); + + foreach ($contextlist as $context) { + /** @var content_writer $writer */ + $writer = writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + + if ($context->instanceid == $course1context->instanceid) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_generate_text', 'core_ai'), + date('c', $timecreated1), + ]; + $name = 'action_generate_text'; + $data = $writer->get_related_data($subcontexts, $name); + $this->assertEquals('generate_text', $data->actionname); + $this->assertEquals($course1context->id, $data->contextid); + $this->assertEquals('This is a test prompt 1', $data->prompt); + $this->assertEquals('This is the generated content 1', $data->generatedcontent); + $this->assertEquals('9', $data->prompttokens); + $this->assertEquals('12', $data->completiontoken); + $this->assertEquals(get_string('yes'), $data->success); + } + + if ($context->instanceid == $course2context->instanceid) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_generate_text', 'core_ai'), + date('c', $timecreated2), + ]; + $name = 'action_generate_text'; + $data = $writer->get_related_data($subcontexts, $name); + $this->assertEquals('generate_text', $data->actionname); + $this->assertEquals($course2context->id, $data->contextid); + $this->assertEquals('This is a test prompt 2', $data->prompt); + $this->assertEquals('This is the generated content 2', $data->generatedcontent); + $this->assertEquals('10', $data->prompttokens); + $this->assertEquals('15', $data->completiontoken); + $this->assertEquals(get_string('yes'), $data->success); + } + } + } + + /** + * Test export_user_data() for generate image. + */ + public function test_export_user_data_for_generate_image(): void { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $timecreated1 = $clock->time(); + $action = new generate_image( + contextid: $course1context->id, + userid: $user->id, + prompttext: 'This is a test prompt 1', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image1.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $timecreated2 = $clock->time(); + $action = new generate_image( + contextid: $course2context->id, + userid: $user->id, + prompttext: 'This is a test prompt 2', + quality: 'hd', + aspectratio: 'portrait', + numimages: 2, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image2.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + $method->invoke($manager, $provider, $action, $actionresponse); + + // Retrieve the user's context ids. + $contextlist = provider::get_contexts_for_userid($user->id); + $approvedcontextlist = new approved_contextlist($user, 'core_ai', $contextlist->get_contextids()); + provider::export_user_data($approvedcontextlist); + + foreach ($contextlist as $context) { + /** @var content_writer $writer */ + $writer = writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + + if ($context->instanceid == $course1context->instanceid) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_generate_image', 'core_ai'), + date('c', $timecreated1), + ]; + $name = 'action_generate_image'; + $data = $writer->get_related_data($subcontexts, $name); + $this->assertEquals('generate_image', $data->actionname); + $this->assertEquals($course1context->id, $data->contextid); + $this->assertEquals('This is a test prompt 1', $data->prompt); + $this->assertEquals('1', $data->numberimages); + $this->assertEquals('hd', $data->quality); + $this->assertEquals('square', $data->aspectratio); + $this->assertEquals('vivid', $data->style); + $this->assertEquals('https://example.com/image1.png', $data->sourceurl); + $this->assertEquals(get_string('yes'), $data->success); + } + + if ($context->instanceid == $course2context->instanceid) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_generate_image', 'core_ai'), + date('c', $timecreated2), + ]; + $name = 'action_generate_image'; + $data = $writer->get_related_data($subcontexts, $name); + $this->assertEquals('generate_image', $data->actionname); + $this->assertEquals($course2context->id, $data->contextid); + $this->assertEquals('This is a test prompt 2', $data->prompt); + $this->assertEquals('2', $data->numberimages); + $this->assertEquals('hd', $data->quality); + $this->assertEquals('portrait', $data->aspectratio); + $this->assertEquals('vivid', $data->style); + $this->assertEquals('https://example.com/image2.png', $data->sourceurl); + $this->assertEquals(get_string('yes'), $data->success); + } + } + } + + /** + * Test export_user_data() for summarise text. + */ + public function test_export_user_data_for_summarise_text(): void { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $timecreated1 = $clock->time(); + $action = new summarise_text( + contextid: $course1context->id, + userid: $user->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $timecreated2 = $clock->time(); + $action = new summarise_text( + contextid: $course2context->id, + userid: $user->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + $method->invoke($manager, $provider, $action, $actionresponse); + + // Retrieve the user's context ids. + $contextlist = provider::get_contexts_for_userid($user->id); + $approvedcontextlist = new approved_contextlist($user, 'core_ai', $contextlist->get_contextids()); + provider::export_user_data($approvedcontextlist); + + foreach ($contextlist as $context) { + /** @var content_writer $writer */ + $writer = writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + + if ($context->instanceid == $course1context->instanceid) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_summarise_text', 'core_ai'), + date('c', $timecreated1), + ]; + $name = 'action_summarise_text'; + $data = $writer->get_related_data($subcontexts, $name); + $this->assertEquals('summarise_text', $data->actionname); + $this->assertEquals($course1context->id, $data->contextid); + $this->assertEquals('This is a test prompt 1', $data->prompt); + $this->assertEquals('This is the generated content 1', $data->generatedcontent); + $this->assertEquals('9', $data->prompttokens); + $this->assertEquals('12', $data->completiontoken); + $this->assertEquals(get_string('yes'), $data->success); + } + + if ($context->instanceid == $course2context->instanceid) { + $subcontexts = [ + get_string('ai', 'core_ai'), + get_string('action_summarise_text', 'core_ai'), + date('c', $timecreated2), + ]; + $name = 'action_summarise_text'; + $data = $writer->get_related_data($subcontexts, $name); + $this->assertEquals('summarise_text', $data->actionname); + $this->assertEquals($course2context->id, $data->contextid); + $this->assertEquals('This is a test prompt 2', $data->prompt); + $this->assertEquals('This is the generated content 2', $data->generatedcontent); + $this->assertEquals('10', $data->prompttokens); + $this->assertEquals('15', $data->completiontoken); + $this->assertEquals(get_string('yes'), $data->success); + } + } + } + + /** + * Test delete_data_for_all_users_in_context() for AI policy. + */ + public function test_delete_data_for_all_users_in_context_for_policy(): void { + global $DB; + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + + // Set the user policy. + manager::set_user_policy($user1->id, $course1context->id); + manager::set_user_policy($user2->id, $course1context->id); + manager::set_user_policy($user3->id, $course2context->id); + + provider::delete_data_for_all_users_in_context($course1context); + + // Verify all policy data for Course 1 has been deleted. + $datas = $DB->get_records('ai_policy_register', ['contextid' => $course1context->id]); + $this->assertCount(0, $datas); + + // Verify policy data for Course 2 are still present. + $datas = $DB->get_records('ai_policy_register', ['contextid' => $course2context->id]); + $this->assertCount(1, $datas); + } + + /** + * Test delete_data_for_all_users_in_context() for generate text. + */ + public function test_delete_data_for_all_users_in_context_for_generate_text(): void { + global $DB; + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new generate_text( + contextid: $course1context->id, + userid: $user->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new generate_text( + contextid: $course2context->id, + userid: $user->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + provider::delete_data_for_all_users_in_context($course1context); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_generate_text', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->responseid); + $this->assertEquals('', $record->fingerprint); + $this->assertEquals('', $record->generatedcontent); + + $record = $DB->get_record('ai_action_generate_text', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->responseid); + $this->assertNotEquals('', $record->fingerprint); + $this->assertNotEquals('', $record->generatedcontent); + } + + /** + * Test delete_data_for_all_users_in_context() for generate image. + */ + public function test_delete_data_for_all_users_in_context_for_generate_image(): void { + global $DB; + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new generate_image( + contextid: $course1context->id, + userid: $user->id, + prompttext: 'This is a test prompt 1', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image1.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new generate_image( + contextid: $course2context->id, + userid: $user->id, + prompttext: 'This is a test prompt 2', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image2.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + provider::delete_data_for_all_users_in_context($course1context); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_generate_image', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->sourceurl); + $this->assertEquals('', $record->revisedprompt); + + $record = $DB->get_record('ai_action_generate_image', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->sourceurl); + $this->assertNotEquals('', $record->revisedprompt); + } + + /** + * Test delete_data_for_all_users_in_context() for summarise text. + */ + public function test_delete_data_for_all_users_in_context_for_summarise_text(): void { + global $DB; + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new summarise_text( + contextid: $course1context->id, + userid: $user->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new summarise_text( + contextid: $course2context->id, + userid: $user->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + provider::delete_data_for_all_users_in_context($course1context); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_summarise_text', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->responseid); + $this->assertEquals('', $record->fingerprint); + $this->assertEquals('', $record->generatedcontent); + + $record = $DB->get_record('ai_action_summarise_text', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->responseid); + $this->assertNotEquals('', $record->fingerprint); + $this->assertNotEquals('', $record->generatedcontent); + } + + /** + * Test delete_data_for_all_users_in_context() for generate text. + */ + public function test_delete_data_for_user_for_generate_text(): void { + global $DB; + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new generate_text( + contextid: $coursecontext->id, + userid: $user1->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new generate_text( + contextid: $coursecontext->id, + userid: $user2->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + $contextlist = provider::get_contexts_for_userid($user1->id); + $approvedcontextlist = new approved_contextlist($user1, 'core_ai', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_generate_text', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->responseid); + $this->assertEquals('', $record->fingerprint); + $this->assertEquals('', $record->generatedcontent); + + $record = $DB->get_record('ai_action_generate_text', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->responseid); + $this->assertNotEquals('', $record->fingerprint); + $this->assertNotEquals('', $record->generatedcontent); + } + + /** + * Test delete_data_for_all_users_in_context() for generate image. + */ + public function test_delete_data_for_user_for_generate_image(): void { + global $DB; + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new generate_image( + contextid: $coursecontext->id, + userid: $user1->id, + prompttext: 'This is a test prompt 1', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image1.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new generate_image( + contextid: $coursecontext->id, + userid: $user2->id, + prompttext: 'This is a test prompt 2', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image2.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + $contextlist = provider::get_contexts_for_userid($user1->id); + $approvedcontextlist = new approved_contextlist($user1, 'core_ai', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_generate_image', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->sourceurl); + $this->assertEquals('', $record->revisedprompt); + + $record = $DB->get_record('ai_action_generate_image', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->sourceurl); + $this->assertNotEquals('', $record->revisedprompt); + } + + /** + * Test delete_data_for_all_users_in_context() for summarise text. + */ + public function test_delete_data_for_user_for_summarise_text(): void { + global $DB; + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new summarise_text( + contextid: $coursecontext->id, + userid: $user1->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new summarise_text( + contextid: $coursecontext->id, + userid: $user2->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + $contextlist = provider::get_contexts_for_userid($user1->id); + $approvedcontextlist = new approved_contextlist($user1, 'core_ai', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_summarise_text', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->responseid); + $this->assertEquals('', $record->fingerprint); + $this->assertEquals('', $record->generatedcontent); + + $record = $DB->get_record('ai_action_summarise_text', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->responseid); + $this->assertNotEquals('', $record->fingerprint); + $this->assertNotEquals('', $record->generatedcontent); + } + + /** + * Test get_users_in_context() for policy. + */ + public function test_get_users_in_context_for_policy(): void { + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + + manager::set_user_policy($user1->id, $course1context->id); + manager::set_user_policy($user2->id, $course2context->id); + + // The user list for course1context should return user1. + $userlist = new \core_privacy\local\request\userlist($course1context, 'core_ai'); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $this->assertTrue(in_array($user1->id, $userlist->get_userids())); + $this->assertFalse(in_array($user2->id, $userlist->get_userids())); + + // The user list for course2context should return user2. + $userlist = new \core_privacy\local\request\userlist($course2context, 'core_ai'); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $this->assertTrue(in_array($user2->id, $userlist->get_userids())); + $this->assertFalse(in_array($user1->id, $userlist->get_userids())); + } + + /** + * Test get_users_in_context() for generate text. + */ + public function test_get_users_in_context_for_generate_text(): void { + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new generate_text( + contextid: $course1context->id, + userid: $user1->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new generate_text( + contextid: $course2context->id, + userid: $user2->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + // The user list for course1context should return user1. + $userlist = new \core_privacy\local\request\userlist($course1context, 'core_ai'); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $this->assertTrue(in_array($user1->id, $userlist->get_userids())); + $this->assertFalse(in_array($user2->id, $userlist->get_userids())); + + // The user list for course2context should return user2. + $userlist = new \core_privacy\local\request\userlist($course2context, 'core_ai'); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $this->assertTrue(in_array($user2->id, $userlist->get_userids())); + $this->assertFalse(in_array($user1->id, $userlist->get_userids())); + } + + /** + * Test get_users_in_context() for generate image. + */ + public function test_get_users_in_context_for_generate_image(): void { + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new generate_image( + contextid: $course1context->id, + userid: $user1->id, + prompttext: 'This is a test prompt 1', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image1.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new generate_image( + contextid: $course2context->id, + userid: $user2->id, + prompttext: 'This is a test prompt 2', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image2.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + // The user list for course1context should return user1. + $userlist = new \core_privacy\local\request\userlist($course1context, 'core_ai'); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $this->assertTrue(in_array($user1->id, $userlist->get_userids())); + $this->assertFalse(in_array($user2->id, $userlist->get_userids())); + + // The user list for course2context should return user2. + $userlist = new \core_privacy\local\request\userlist($course2context, 'core_ai'); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $this->assertTrue(in_array($user2->id, $userlist->get_userids())); + $this->assertFalse(in_array($user1->id, $userlist->get_userids())); + } + + /** + * Test get_users_in_context() for summarise text. + */ + public function test_get_users_in_context_for_summarise_text(): void { + global $DB; + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new summarise_text( + contextid: $course1context->id, + userid: $user1->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new summarise_text( + contextid: $course2context->id, + userid: $user2->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $method->invoke($manager, $provider, $action, $actionresponse); + + // The user list for course1context should return user1. + $userlist = new \core_privacy\local\request\userlist($course1context, 'core_ai'); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $this->assertTrue(in_array($user1->id, $userlist->get_userids())); + $this->assertFalse(in_array($user2->id, $userlist->get_userids())); + + // The user list for course2context should return user2. + $userlist = new \core_privacy\local\request\userlist($course2context, 'core_ai'); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $this->assertTrue(in_array($user2->id, $userlist->get_userids())); + $this->assertFalse(in_array($user1->id, $userlist->get_userids())); + } + + /** + * Test delete_data_for_users() for policy. + */ + public function test_delete_data_for_users_for_policy(): void { + global $DB; + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + + // Set the user policy. + manager::set_user_policy($user1->id, $course1context->id); + manager::set_user_policy($user2->id, $course1context->id); + manager::set_user_policy($user3->id, $course2context->id); + + $userlist = new \core_privacy\local\request\userlist($course1context, 'core_ai'); + provider::get_users_in_context($userlist); + $approvedlist = new approved_userlist($course1context, 'core_ai', $userlist->get_userids()); + provider::delete_data_for_users($approvedlist); + + // Verify all policy data for user1 and user2 have been deleted. + $this->assertFalse($DB->record_exists('ai_policy_register', ['userid' => $user1->id])); + $this->assertFalse($DB->record_exists('ai_policy_register', ['userid' => $user2->id])); + + // Verify policy data for user3 is still present. + $this->assertTrue($DB->record_exists('ai_policy_register', ['userid' => $user3->id])); + } + + /** + * Test delete_data_for_users() for generate text. + */ + public function test_delete_data_for_users_for_generate_text(): void { + global $DB; + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new generate_text( + contextid: $course1context->id, + userid: $user1->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new generate_text( + contextid: $course2context->id, + userid: $user2->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_generate_text( + success: true, + actionname: 'generate_text', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + $userlist = new \core_privacy\local\request\userlist($course1context, 'core_ai'); + provider::get_users_in_context($userlist); + $approvedlist = new approved_userlist($course1context, 'core_ai', $userlist->get_userids()); + provider::delete_data_for_users($approvedlist); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_generate_text', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->responseid); + $this->assertEquals('', $record->fingerprint); + $this->assertEquals('', $record->generatedcontent); + + $record = $DB->get_record('ai_action_generate_text', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->responseid); + $this->assertNotEquals('', $record->fingerprint); + $this->assertNotEquals('', $record->generatedcontent); + } + + /** + * Test delete_data_for_users() for generate image. + */ + public function test_delete_data_for_users_for_generate_image(): void { + global $DB; + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new generate_image( + contextid: $course1context->id, + userid: $user1->id, + prompttext: 'This is a test prompt 1', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image1.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new generate_image( + contextid: $course2context->id, + userid: $user2->id, + prompttext: 'This is a test prompt 2', + quality: 'hd', + aspectratio: 'square', + numimages: 1, + style: 'vivid', + ); + + $body = [ + 'revisedprompt' => 'This is a revised prompt', + 'sourceurl' => 'https://example.com/image2.png', + ]; + $actionresponse = new response_generate_image( + success: true, + actionname: 'generate_image', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + $userlist = new \core_privacy\local\request\userlist($course1context, 'core_ai'); + provider::get_users_in_context($userlist); + $approvedlist = new approved_userlist($course1context, 'core_ai', $userlist->get_userids()); + provider::delete_data_for_users($approvedlist); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_generate_image', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->sourceurl); + $this->assertEquals('', $record->revisedprompt); + + $record = $DB->get_record('ai_action_generate_image', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->sourceurl); + $this->assertNotEquals('', $record->revisedprompt); + } + + /** + * Test delete_data_for_users() for generate image. + */ + public function test_delete_data_for_users_for_summarise_text(): void { + global $DB; + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + $clock = $this->mock_clock_with_frozen(); + + $action = new summarise_text( + contextid: $course1context->id, + userid: $user->id, + prompttext: 'This is a test prompt 1', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 1', + 'finishreason' => 'stop', + 'prompttokens' => 9, + 'completiontokens' => 12, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + + $provider = new \aiprovider_openai\provider(); + $manager = new manager(); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult1 = $method->invoke($manager, $provider, $action, $actionresponse); + + $clock->bump(2); + $action = new summarise_text( + contextid: $course2context->id, + userid: $user->id, + prompttext: 'This is a test prompt 2', + ); + + $body = [ + 'id' => 'chatcmpl-123', + 'fingerprint' => 'fp_44709d6fcb', + 'generatedcontent' => 'This is the generated content 2', + 'finishreason' => 'stop', + 'prompttokens' => 10, + 'completiontokens' => 15, + ]; + $actionresponse = new response_summarise_text( + success: true, + actionname: 'summarise_text', + ); + $actionresponse->set_response($body); + $method = new \ReflectionMethod($manager, 'store_action_result'); + $storeresult2 = $method->invoke($manager, $provider, $action, $actionresponse); + + $userlist = new \core_privacy\local\request\userlist($course1context, 'core_ai'); + provider::get_users_in_context($userlist); + $approvedlist = new approved_userlist($course1context, 'core_ai', $userlist->get_userids()); + provider::delete_data_for_users($approvedlist); + + $actionid1 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult1]); + $actionid2 = $DB->get_field('ai_action_register', 'actionid', ['id' => $storeresult2]); + + $record = $DB->get_record('ai_action_summarise_text', ['id' => $actionid1]); + $this->assertEquals('', $record->prompt); + $this->assertEquals('', $record->responseid); + $this->assertEquals('', $record->fingerprint); + $this->assertEquals('', $record->generatedcontent); + + $record = $DB->get_record('ai_action_summarise_text', ['id' => $actionid2]); + $this->assertNotEquals('', $record->prompt); + $this->assertNotEquals('', $record->responseid); + $this->assertNotEquals('', $record->fingerprint); + $this->assertNotEquals('', $record->generatedcontent); + } + /** * Test get_name. */ diff --git a/lang/en/ai.php b/lang/en/ai.php index 7de721146f9..7eeef3cf583 100644 --- a/lang/en/ai.php +++ b/lang/en/ai.php @@ -17,7 +17,7 @@ /** * Strings for component 'ai', language 'en' * - * @package core_ai + * @package core * @category string * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -61,7 +61,40 @@ Each action can be enabled or disabled for this placement.'; $string['placementsettings'] = 'Placement specific settings'; $string['placementsettings_desc'] = 'These settings control various aspects of this AI placement.
They control how the placement connects to the AI service, and related operations'; -$string['privacy:metadata'] = 'The AI subsystem currently does not store any user data.'; +$string['privacy:metadata:ai_action_generate_image'] = 'A table storing the image generation requests made by users.'; +$string['privacy:metadata:ai_action_generate_image:aspectratio'] = 'The aspect ratio of the generated images.'; +$string['privacy:metadata:ai_action_generate_image:numberimages'] = 'The number of images generated.'; +$string['privacy:metadata:ai_action_generate_image:prompt'] = 'The prompt for the image generation request.'; +$string['privacy:metadata:ai_action_generate_image:quality'] = 'The quality of the generated images.'; +$string['privacy:metadata:ai_action_generate_image:revisedprompt'] = 'The revised prompt of the generated images.'; +$string['privacy:metadata:ai_action_generate_image:sourceurl'] = 'The source URL of the generated images.'; +$string['privacy:metadata:ai_action_generate_image:style'] = 'The style of the generated images.'; +$string['privacy:metadata:ai_action_generate_text'] = 'A table storing the text generation requests made by users.'; +$string['privacy:metadata:ai_action_generate_text:completiontoken'] = 'The completion tokens used to generate the text.'; +$string['privacy:metadata:ai_action_generate_text:fingerprint'] = 'The unique hash representing the state/version of the model and content.'; +$string['privacy:metadata:ai_action_generate_text:generatedcontent'] = 'The actual text generated by the AI model based on the input prompt.'; +$string['privacy:metadata:ai_action_generate_text:prompt'] = 'The prompt for the text generation request.'; +$string['privacy:metadata:ai_action_generate_text:prompttokens'] = 'The prompt tokens used to generate the text.'; +$string['privacy:metadata:ai_action_generate_text:responseid'] = 'The ID of the response.'; +$string['privacy:metadata:ai_action_register'] = 'A table storing the action requests made by users.'; +$string['privacy:metadata:ai_action_register:actionid'] = 'The ID of the action request.'; +$string['privacy:metadata:ai_action_register:actionname'] = 'The action name of the request.'; +$string['privacy:metadata:ai_action_register:provider'] = 'The name of the provider that handled the request.'; +$string['privacy:metadata:ai_action_register:success'] = 'The state of the action request.'; +$string['privacy:metadata:ai_action_register:timecreated'] = 'The created time of the request.'; +$string['privacy:metadata:ai_action_register:timecompleted'] = 'The completed time of the request.'; +$string['privacy:metadata:ai_action_register:userid'] = 'The ID of the user who made the request.'; +$string['privacy:metadata:ai_policy_register'] = 'A table storing the status of the AI policy acceptance for each user.'; +$string['privacy:metadata:ai_policy_register:userid'] = 'The ID of the user whose data was saved.'; +$string['privacy:metadata:ai_policy_register:contextid'] = 'The ID of the context whose data was saved.'; +$string['privacy:metadata:ai_policy_register:timeaccepted'] = 'The time the user accepted the AI policy.'; +$string['privacy:metadata:ai_action_summarise_text'] = 'A table storing the summarise text requests made by users.'; +$string['privacy:metadata:ai_action_summarise_text:completiontoken'] = 'The completion tokens used to summarise the text.'; +$string['privacy:metadata:ai_action_summarise_text:fingerprint'] = 'The unique hash representing the state/version of the model and content.'; +$string['privacy:metadata:ai_action_summarise_text:generatedcontent'] = 'The actual text generated by the AI model based on the input prompt.'; +$string['privacy:metadata:ai_action_summarise_text:prompt'] = 'The prompt for the summarise text request.'; +$string['privacy:metadata:ai_action_summarise_text:prompttokens'] = 'The prompt tokens used to summarise the text.'; +$string['privacy:metadata:ai_action_summarise_text:responseid'] = 'The ID of the response.'; $string['provideractionsettings'] = 'Provider actions'; $string['provideractionsettings_desc'] = 'These settings are for the actions that are supported by this AI provider.
Each action can be enabled or disabled for this provider.