ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK \Vd db/hooks.phpnu[. /** * Hook listener callbacks for the Azure AI provider. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $callbacks = [ [ 'hook' => \core_ai\hook\after_ai_provider_form_hook::class, 'callback' => \aiprovider_azureai\hook_listener::class . '::set_form_definition_for_aiprovider_azureai', ], ]; PK \6͛_lang/en/deprecated.txtnu[action_apiversion,aiprovider_azureai action_deployment,aiprovider_azureai action_deployment_desc,aiprovider_azureai action_systeminstruction,aiprovider_azureai action_systeminstruction_desc,aiprovider_azureai apikey_desc,aiprovider_azureai deployment,aiprovider_azureai deployment_desc,aiprovider_azureai enableglobalratelimit,aiprovider_azureai enableglobalratelimit_desc,aiprovider_azureai enableuserratelimit,aiprovider_azureai enableuserratelimit_desc,aiprovider_azureai endpoint_desc,aiprovider_azureai globalratelimit,aiprovider_azureai globalratelimit_desc,aiprovider_azureai userratelimit,aiprovider_azureai userratelimit_desc,aiprovider_azureai PK \M llang/en/aiprovider_azureai.phpnu[. /** * Strings for component aiprovider_azureai, language 'en'. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['action:explain_text:apiversion'] = 'API version'; $string['action:explain_text:deployment'] = 'Deployment ID'; $string['action:explain_text:deployment_help'] = 'The deployment ID that relates to the API endpoint the provider uses for this action.'; $string['action:explain_text:systeminstruction'] = 'System instruction'; $string['action:explain_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['action:generate_image:apiversion'] = 'API version'; $string['action:generate_image:deployment'] = 'Deployment ID'; $string['action:generate_image:deployment_help'] = 'The deployment ID that relates to the API endpoint the provider uses for this action.'; $string['action:generate_text:apiversion'] = 'API version'; $string['action:generate_text:deployment'] = 'Deployment ID'; $string['action:generate_text:deployment_help'] = 'The deployment ID that relates to the API endpoint the provider uses for this action.'; $string['action:generate_text:systeminstruction'] = 'System instruction'; $string['action:generate_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['action:summarise_text:apiversion'] = 'API version'; $string['action:summarise_text:deployment'] = 'Deployment ID'; $string['action:summarise_text:deployment_help'] = 'The deployment ID that relates to the API endpoint the provider uses for this action.'; $string['action:summarise_text:systeminstruction'] = 'System instruction'; $string['action:summarise_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['apikey'] = 'Azure AI API key'; $string['apikey_help'] = 'Enter your Azure AI API key.'; $string['endpoint'] = 'Azure AI API endpoint'; $string['endpoint_help'] = 'Enter the endpoint URL for your Azure AI API in the following format: https://YOUR_RESOURCE_NAME.openai.azure.com'; $string['pluginname'] = 'Azure AI API provider'; $string['privacy:metadata'] = 'The Azure AI API provider plugin does not store any personal data.'; $string['privacy:metadata:aiprovider_azureai:externalpurpose'] = 'This information is sent to the Azure API in order for a response to be generated. Your Azure AI account settings may change how Microsoft stores and retains this data. No user data is explicitly sent to Microsoft or stored in Moodle LMS by this plugin.'; $string['privacy:metadata:aiprovider_azureai:model'] = 'The model used to generate the response.'; $string['privacy:metadata:aiprovider_azureai:numberimages'] = 'When generating images the number of images used in the response.'; $string['privacy:metadata:aiprovider_azureai:prompttext'] = 'The user entered text prompt used to generate the response.'; $string['privacy:metadata:aiprovider_azureai:responseformat'] = 'The format of the response when generating images.'; // Deprecated since Moodle 5.0. $string['action_apiversion'] = 'API version'; $string['action_deployment'] = 'Deployment ID'; $string['action_deployment_desc'] = 'The deployment ID that relates to the API endpoint the provider uses for this action.'; $string['action_systeminstruction'] = 'System instruction'; $string['action_systeminstruction_desc'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['apikey_desc'] = 'Enter your Azure AI API key.'; $string['deployment'] = 'Azure AI API deployment name'; $string['deployment_desc'] = 'Enter the deployment name for your Azure AI API.'; $string['enableglobalratelimit'] = 'Set site-wide rate limit'; $string['enableglobalratelimit_desc'] = 'Limit the number of requests that the Azure AI API provider can receive across the entire site every hour.'; $string['enableuserratelimit'] = 'Set user rate limit'; $string['enableuserratelimit_desc'] = 'Limit the number of requests each user can make to the Azure AI API provider every hour.'; $string['endpoint_desc'] = 'Enter the endpoint URL for your Azure AI API in the following format: https://YOUR_RESOURCE_NAME.openai.azure.com'; $string['globalratelimit'] = 'Maximum number of site-wide requests'; $string['globalratelimit_desc'] = 'The number of site-wide requests allowed per hour.'; $string['userratelimit'] = 'Maximum number of requests per user'; $string['userratelimit_desc'] = 'The number of requests allowed per hour, per user.'; PK \!cNGG version.phpnu[. /** * Version information for aiprovider_azureai. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'aiprovider_azureai'; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->maturity = MATURITY_STABLE; PK \{Z Z +classes/form/action_generate_image_form.phpnu[. namespace aiprovider_azureai\form; use core_ai\form\action_settings_form; /** * Generate image action provider settings form. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_generate_image_form extends action_settings_form { #[\Override] protected function definition() { $mform = $this->_form; $actionconfig = $this->_customdata['actionconfig']['settings'] ?? []; $returnurl = $this->_customdata['returnurl'] ?? null; $actionname = $this->_customdata['actionname']; $action = $this->_customdata['action']; $providerid = $this->_customdata['providerid'] ?? 0; // Add API deployment name. $mform->addElement( 'text', 'deployment', get_string("action:{$actionname}:deployment", 'aiprovider_azureai'), 'maxlength="255" size="20"', ); $mform->setType('deployment', PARAM_ALPHANUMEXT); $mform->addRule('deployment', null, 'required', null, 'client'); $mform->setDefault('deployment', $actionconfig['deployment'] ?? ''); $mform->addHelpButton('deployment', "action:{$actionname}:deployment", 'aiprovider_azureai'); // Add API version. $mform->addElement( 'text', 'apiversion', get_string("action:{$actionname}:apiversion", 'aiprovider_azureai'), 'maxlength="255" size="30"', ); $mform->setType('apiversion', PARAM_ALPHANUMEXT); $mform->addRule('apiversion', null, 'required', null, 'client'); $mform->setDefault('apiversion', $actionconfig['apiversion'] ?? '2024-06-01'); if ($returnurl) { $mform->addElement('hidden', 'returnurl', $returnurl); $mform->setType('returnurl', PARAM_LOCALURL); } // Add the action class as a hidden field. $mform->addElement('hidden', 'action', $action); $mform->setType('action', PARAM_TEXT); // Add the provider class as a hidden field. $mform->addElement('hidden', 'provider', 'aiprovider_azureai'); $mform->setType('provider', PARAM_TEXT); // Add the provider id as a hidden field. $mform->addElement('hidden', 'providerid', $providerid); $mform->setType('providerid', PARAM_INT); $this->set_data($actionconfig); } } PK \.t6*classes/form/action_generate_text_form.phpnu[. namespace aiprovider_azureai\form; use core_ai\form\action_settings_form; /** * Generate text action provider settings form. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_generate_text_form extends action_settings_form { #[\Override] protected function definition() { $mform = $this->_form; $actionconfig = $this->_customdata['actionconfig']['settings'] ?? []; $returnurl = $this->_customdata['returnurl'] ?? null; $actionname = $this->_customdata['actionname']; $action = $this->_customdata['action']; $providerid = $this->_customdata['providerid'] ?? 0; // Add API deployment name. $mform->addElement( 'text', 'deployment', get_string("action:{$actionname}:deployment", 'aiprovider_azureai'), 'maxlength="255" size="20"', ); $mform->setType('deployment', PARAM_ALPHANUMEXT); $mform->addRule('deployment', null, 'required', null, 'client'); $mform->setDefault('deployment', $actionconfig['deployment'] ?? ''); $mform->addHelpButton('deployment', "action:{$actionname}:deployment", 'aiprovider_azureai'); // Add API version. $mform->addElement( 'text', 'apiversion', get_string("action:{$actionname}:apiversion", 'aiprovider_azureai'), 'maxlength="255" size="30"', ); $mform->setType('apiversion', PARAM_ALPHANUMEXT); $mform->addRule('apiversion', null, 'required', null, 'client'); $mform->setDefault('apiversion', $actionconfig['apiversion'] ?? '2024-06-01'); // System Instructions. $mform->addElement( 'textarea', 'systeminstruction', get_string("action:{$actionname}:systeminstruction", 'aiprovider_azureai'), 'wrap="virtual" rows="5" cols="20"', ); $mform->setType('systeminstruction', PARAM_TEXT); $mform->setDefault('systeminstruction', $actionconfig['systeminstruction'] ?? $action::get_system_instruction()); $mform->addHelpButton('systeminstruction', "action:{$actionname}:systeminstruction", 'aiprovider_azureai'); if ($returnurl) { $mform->addElement('hidden', 'returnurl', $returnurl); $mform->setType('returnurl', PARAM_LOCALURL); } // Add the action class as a hidden field. $mform->addElement('hidden', 'action', $action); $mform->setType('action', PARAM_TEXT); // Add the provider class as a hidden field. $mform->addElement('hidden', 'provider', 'aiprovider_azureai'); $mform->setType('provider', PARAM_TEXT); // Add the provider id as a hidden field. $mform->addElement('hidden', 'providerid', $providerid); $mform->setType('providerid', PARAM_INT); $this->set_data($actionconfig); } } PK \ūA  classes/privacy/provider.phpnu[. namespace aiprovider_azureai\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\userlist; /** * Privacy Subsystem for Azure AI provider implementing null_provider. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { #[\Override] public static function get_metadata(collection $collection): collection { $collection->add_external_location_link('aiprovider_azureai', [ 'prompttext' => 'privacy:metadata:aiprovider_azureai:prompttext', 'model' => 'privacy:metadata:aiprovider_azureai:model', 'numberimages' => 'privacy:metadata:aiprovider_azureai:numberimages', 'responseformat' => 'privacy:metadata:aiprovider_azureai:responseformat', ], 'privacy:metadata:aiprovider_azureai:externalpurpose'); return $collection; } #[\Override] public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } #[\Override] public static function get_users_in_context(userlist $userlist) { } #[\Override] public static function export_user_data(approved_contextlist $contextlist) { } #[\Override] public static function delete_data_for_all_users_in_context(\context $context) { } /** * 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) { } #[\Override] public static function delete_data_for_user(approved_contextlist $contextlist) { } } PK \hІ"classes/process_generate_image.phpnu[. namespace aiprovider_azureai; use core\http_client; use core_ai\ai_image; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; /** * Class process image generation. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_generate_image extends abstract_processor { /** @var int The number of images to generate dall-e-3 only supports 1 */ private int $numberimages = 1; #[\Override] protected function get_endpoint(): UriInterface { $url = rtrim($this->provider->config['endpoint'], '/') . '/openai/deployments/' . $this->get_deployment_name() . '/images/generations?api-version=' . $this->get_api_version(); return new Uri($url); } #[\Override] protected function query_ai_api(): array { $response = parent::query_ai_api(); // If the request was successful, save the URL to a file. if ($response['success']) { $fileobj = $this->url_to_file( $this->action->get_configuration('userid'), $response['sourceurl'] ); // Add the file to the response, so the calling placement can do whatever they want with it. $response['draftfile'] = $fileobj; } return $response; } /** * Convert the given aspect ratio to an image size * that is compatible with the azureai API. * * @param string $ratio The aspect ratio of the image. * @return string The size of the image. * @throws \coding_exception */ private function calculate_size(string $ratio): string { if ($ratio === 'square') { $size = '1024x1024'; } else if ($ratio === 'landscape') { $size = '1792x1024'; } else if ($ratio === 'portrait') { $size = '1024x1792'; } else { throw new \coding_exception('Invalid aspect ratio: ' . $ratio); } return $size; } #[\Override] protected function create_request_object(string $userid): RequestInterface { return new Request( method: 'POST', uri: '', headers: [ 'Content-Type' => 'application/json', ], body: json_encode((object) [ 'prompt' => $this->action->get_configuration('prompttext'), 'n' => $this->numberimages, 'quality' => $this->action->get_configuration('quality'), 'size' => $this->calculate_size($this->action->get_configuration('aspectratio')), 'style' => $this->action->get_configuration('style'), 'user' => $userid, ]), ); } #[\Override] protected function handle_api_success(ResponseInterface $response): array { $responsebody = $response->getBody(); $bodyobj = json_decode($responsebody->getContents()); return [ 'success' => true, 'sourceurl' => $bodyobj->data[0]->url, 'revisedprompt' => $bodyobj->data[0]->revised_prompt, ]; } /** * Convert the url for the image to a file. * * Placements can't interact with the provider AI directly, * therefore we need to provide the image file in a format that can * be used by placements. So we use the file API. * * @param int $userid The user id. * @param string $url The URL to the image. * @return \stored_file The file object. */ private function url_to_file(int $userid, string $url): \stored_file { global $CFG; require_once("{$CFG->libdir}/filelib.php"); // Azure AI doesn't always return unique file names, but does return unique URLS. // Therefore, some processing is needed to get a unique filename. $parsedurl = parse_url($url, PHP_URL_PATH); // Parse the URL to get the path. $fileext = pathinfo($parsedurl, PATHINFO_EXTENSION); // Get the file extension. $filename = substr(hash('sha512', ($url . $userid)), 0, 16) . '.' . $fileext; $client = \core\di::get(http_client::class); // Download the image and add the watermark. $downloadtmpdir = make_request_directory(); $tempdst = $downloadtmpdir . $filename; $client->get($url, [ 'sink' => $tempdst, 'timeout' => $CFG->repositorygetfiletimeout, ]); $image = new ai_image($tempdst); $image->add_watermark()->save(); // We put the file in the user draft area initially. // Placements (on behalf of the user) can then move it to the correct location. $fileinfo = new \stdClass(); $fileinfo->contextid = \context_user::instance($userid)->id; $fileinfo->filearea = 'draft'; $fileinfo->component = 'user'; $fileinfo->itemid = file_get_unused_draft_itemid(); $fileinfo->filepath = '/'; $fileinfo->filename = $filename; $fs = get_file_storage(); return $fs->create_file_from_string($fileinfo, file_get_contents($tempdst)); } } PK \"classes/process_summarise_text.phpnu[. namespace aiprovider_azureai; /** * Class process text summarisation. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_summarise_text extends process_generate_text { } PK \inclasses/hook_listener.phpnu[. namespace aiprovider_azureai; use core_ai\hook\after_ai_provider_form_hook; /** * Hook listener for Azure AI provider. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class hook_listener { /** * Hook listener for the Azure AI instance setup form. * * @param after_ai_provider_form_hook $hook The hook to add to the AI instance setup. */ public static function set_form_definition_for_aiprovider_azureai(after_ai_provider_form_hook $hook): void { if ($hook->plugin !== 'aiprovider_azureai') { return; } $mform = $hook->mform; // Required setting to store azureai API key. $mform->addElement( 'passwordunmask', 'apikey', get_string('apikey', 'aiprovider_azureai'), ['size' => 75], ); $mform->addHelpButton('apikey', 'apikey', 'aiprovider_azureai'); $mform->addRule('apikey', get_string('required'), 'required', null, 'client'); // Setting to store AzureAI endpoint URL. $mform->addElement( 'text', 'endpoint', get_string('endpoint', 'aiprovider_azureai'), ['size' => 25], ); $mform->setType('endpoint', PARAM_URL); $mform->addHelpButton('endpoint', 'endpoint', 'aiprovider_azureai'); $mform->addRule('endpoint', get_string('required'), 'required', null, 'client'); } } PK \7classes/abstract_processor.phpnu[. namespace aiprovider_azureai; use core\http_client; use core_ai\process_base; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\RequestOptions; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; /** * Class process text generation. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class abstract_processor extends process_base { /** * Get the endpoint URI. * * @return UriInterface */ abstract protected function get_endpoint(): UriInterface; /** * Get the system instructions. * * @return string */ protected function get_system_instruction(): string { return $this->action::get_system_instruction(); } /** * Get the deployment name. * * @return string */ protected function get_deployment_name(): string { return $this->provider->actionconfig[$this->action::class]['settings']['deployment']; } /** * Get the api version to use. * * @return string */ protected function get_api_version(): string { return $this->provider->actionconfig[$this->action::class]['settings']['apiversion']; } /** * Create the request object to send to the OpenAI API. * * This object contains all the required parameters for the request. * * @param string $userid The user id. * @return RequestInterface The request object to send to the OpenAI API. */ abstract protected function create_request_object( string $userid, ): RequestInterface; /** * Handle a successful response from the external AI api. * * @param ResponseInterface $response The response object. * @return array The response. */ abstract protected function handle_api_success(ResponseInterface $response): array; #[\Override] protected function query_ai_api(): array { $request = $this->create_request_object( userid: $this->provider->generate_userid($this->action->get_configuration('userid')), ); $request = $this->provider->add_authentication_headers($request); $client = \core\di::get(http_client::class); try { // Call the external AI service. $response = $client->send($request, [ 'base_uri' => $this->get_endpoint(), RequestOptions::HTTP_ERRORS => false, ]); } catch (RequestException $e) { // Handle any exceptions. return [ 'success' => false, 'errorcode' => $e->getCode(), 'errormessage' => $e->getMessage(), ]; } // Double-check the response codes, in case of a non 200 that didn't throw an error. $status = $response->getStatusCode(); if ($status === 200) { return $this->handle_api_success($response); } else { return $this->handle_api_error($response); } } /** * Handle an error from the external AI api. * * @param ResponseInterface $response The response object. * @return array The error response. */ protected function handle_api_error(ResponseInterface $response): array { $responsearr = [ 'success' => false, 'errorcode' => $response->getStatusCode(), ]; $status = $response->getStatusCode(); if ($status >= 500 && $status < 600) { $responsearr['errormessage'] = $response->getReasonPhrase(); } else { $bodyobj = json_decode($response->getBody()->getContents()); $responsearr['errormessage'] = $bodyobj->error->message; } return $responsearr; } } PK \nF F classes/provider.phpnu[. namespace aiprovider_azureai; use core_ai\form\action_settings_form; use Psr\Http\Message\RequestInterface; /** * Class provider. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider extends \core_ai\provider { /** * Get the list of actions that this provider supports. * * @return array An array of action class names. */ public static function get_action_list(): array { return [ \core_ai\aiactions\generate_text::class, \core_ai\aiactions\generate_image::class, \core_ai\aiactions\summarise_text::class, \core_ai\aiactions\explain_text::class, ]; } #[\Override] public function add_authentication_headers(RequestInterface $request): RequestInterface { return $request ->withAddedHeader('api-key', $this->config['apikey']); } #[\Override] public static function get_action_settings( string $action, array $customdata = [], ): action_settings_form|bool { $actionname = substr($action, (strrpos($action, '\\') + 1)); $customdata['actionname'] = $actionname; $customdata['action'] = $action; if ($actionname === 'generate_text' || $actionname === 'summarise_text' || $actionname === 'explain_text') { return new form\action_generate_text_form(customdata: $customdata); } else if ($actionname === 'generate_image') { return new form\action_generate_image_form(customdata: $customdata); } return false; } #[\Override] public static function get_action_setting_defaults(string $action): array { $actionname = substr($action, (strrpos($action, '\\') + 1)); $customdata = [ 'actionname' => $actionname, 'action' => $action, ]; if ($actionname === 'generate_text' || $actionname === 'summarise_text' || $actionname === 'explain_text') { $mform = new form\action_generate_text_form(customdata: $customdata); return $mform->get_defaults(); } else if ($actionname === 'generate_image') { $mform = new form\action_generate_image_form(customdata: $customdata); return $mform->get_defaults(); } return []; } /** * Check this provider has the minimal configuration to work. * * @return bool Return true if configured. */ public function is_provider_configured(): bool { return !empty($this->config['apikey']) && !empty($this->config['endpoint']); } } PK \mR* classes/process_explain_text.phpnu[. namespace aiprovider_azureai; /** * Class process text explanation. * * @package aiprovider_azureai * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_explain_text extends process_generate_text { } PK \)!classes/process_generate_text.phpnu[. namespace aiprovider_azureai; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; /** * Class process text generation. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_generate_text extends abstract_processor { #[\Override] protected function get_endpoint(): UriInterface { $url = rtrim($this->provider->config['endpoint'], '/') . '/openai/deployments/' . $this->get_deployment_name() . '/chat/completions?api-version=' . $this->get_api_version(); return new Uri($url); } #[\Override] protected function get_system_instruction(): string { return $this->provider->actionconfig[$this->action::class]['settings']['systeminstruction']; } #[\Override] protected function create_request_object(string $userid): RequestInterface { // Create the user object. $userobj = new \stdClass(); $userobj->role = 'user'; $userobj->content = $this->action->get_configuration('prompttext'); // Create the request object. $requestobj = new \stdClass(); $requestobj->user = $userid; // If there is a system string available, use it. $systeminstruction = $this->get_system_instruction(); if (!empty($systeminstruction)) { $systemobj = new \stdClass(); $systemobj->role = 'system'; $systemobj->content = $systeminstruction; $requestobj->messages = [$systemobj, $userobj]; } else { $requestobj->messages = [$userobj]; } return new Request( method: 'POST', uri: '', headers: [ 'Content-Type' => 'application/json', ], body: json_encode($requestobj), ); } /** * Handle a successful response from the external AI api. * * @param ResponseInterface $response The response object. * @return array The response. */ protected function handle_api_success(ResponseInterface $response): array { $responsebody = $response->getBody(); $bodyobj = json_decode($responsebody->getContents()); return [ 'success' => true, 'id' => $bodyobj->id, 'fingerprint' => $bodyobj->system_fingerprint, 'generatedcontent' => $bodyobj->choices[0]->message->content, 'finishreason' => $bodyobj->choices[0]->finish_reason, 'prompttokens' => $bodyobj->usage->prompt_tokens, 'completiontokens' => $bodyobj->usage->completion_tokens, 'model' => $bodyobj->model, ]; } } PK \((tests/fixtures/text_request_success.jsonnu[{ "choices":[ { "content_filter_results":{ "hate":{ "filtered":false, "severity":"safe" }, "self_harm":{ "filtered":false, "severity":"safe" }, "sexual":{ "filtered":false, "severity":"safe" }, "violence":{ "filtered":false, "severity":"safe" } }, "finish_reason":"stop", "index":0, "logprobs":null, "message":{ "content":"Sure, I'm here to help! How can I assist you today?", "role":"assistant" } } ], "created":1721897889, "id":"chatcmpl-9ooaXlMSUIhOkd2pfxKBgpipMynkX", "model":"gpt-4o-2024-05-13", "object":"chat.completion", "prompt_filter_results":[ { "prompt_index":0, "content_filter_results":{ "hate":{ "filtered":false, "severity":"safe" }, "jailbreak":{ "filtered":false, "detected":false }, "self_harm":{ "filtered":false, "severity":"safe" }, "sexual":{ "filtered":false, "severity":"safe" }, "violence":{ "filtered":false, "severity":"safe" } } } ], "system_fingerprint":"fp_abc28019ad", "usage":{ "completion_tokens":14, "prompt_tokens":12, "total_tokens":26 } } PK \]>6)tests/fixtures/image_request_success.jsonnu[{ "created": 1719140500, "data": [ { "revised_prompt": "An image that represents the concept of a 'test'. It could be a variety of things, such as a piece of paper with multiple-choice questions, a scientist in a lab conducting experiments, or a student at a desk studying for an upcoming exam. This image should use vivid colors and clear, detailed imagery to clearly represent the concept of 'test'.", "url": "https:\/\/oaidalleapiprodscus.blob.core.windows.net\/private\/org-EdXlYn9JmBUAo1tZ1QeRcOvg\/user-8GYNL27bMyzS3WcLtkUwREgd\/img-uPMRNi0R9lxPPJYOf4NZUmMY.png?st=2024-06-23T10%3A01%3A40Z&se=2024-06-23T12%3A01%3A40Z&sp=r&sv=2023-11-03&sr=b&rscd=inline&rsct=image\/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-06-23T00%3A13%3A04Z&ske=2024-06-24T00%3A13%3A04Z&sks=b&skv=2023-11-03&sig=bmZqUqQ2QLrmQFSGxTRQoKRVf4C6VyYCZ0aA4Y5%2BGCs%3D" } ] } PK \:Dr44tests/fixtures/test.jpgnu[JFIFHHPhotoshop 3.08BIM(ICC_PROFILEappl scnrRGB XYZ acspAPPLappl-appl rXYZgXYZbXYZ0wtptDchadX,rTRCgTRCbTRCdesc=cprtAdscmXYZ tK>XYZ Zs&XYZ (W3XYZ Rsf32 B&lcurv3descCamera RGB ProfileCamera RGB Profilemluc enUS$esES,LdaDK4deDE,fiFI(frFU<itIT,rnlNL$noNO xptBR(JsvSE*jaJPkoKR2zhTW2zhCNKameran RGB-profiiliRGB-profil fr Kamera000 RGB 000000exOMvj_ RGB r_icϏPerfil RGB para CmaraRGB-kameraprofilRGB-Profil fr Kamerasvg: RGB cϏeNRGB-beskrivelse til KameraRGB-profiel CameratT| RGB \ |Perfil RGB de CmeraProfilo RGB FotocameraCamera RGB ProfileProfil RVB de l appareil-phototextCopyright 2003 Apple Computer Inc., all rights reserved.\ExifMM*  z(2i SONYDSC-H5HH2006:12:15 21:51:49"'0221  4 <D  L0100Ƞ  <% 2006:12:15 21:51:492006:12:15 21:51:49HCC }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?J0׌xǶAq_IݸhRzu^R|ϥ)E%}.]u]VMtˑ׮9Zk]_rjvO[StݖY7޷q$s\x\]Rz4OwNkk7fMI]]mC ~On WJRz=/߫oKmIFچQۧ^Wzw{G}UZl.g=+$Xtgӟߧ){]_6s^6oet䝝ᄋIO7~ΙGs뜞ו*z[qwzZ2k]'Vws-(<cϹ{Y$cmz5N7ݵgګ^S>\rx_댞5UgJھZ]N[~]/u{nxQ~x3n6Vufݬ~n^ݽnVx;rH<)5&嫌J͵q;iM^߫~Rz}=={+9M]K9iַ^n~oM=:) #>ӓۮO=knEtww5wV-4m~"C0z>g'%n].ZwvJWYkͶKZN/Ǟn\ɥ]wv+߫Oc}fV}SݢEsƢR+{՗~۱Wzn֫}^}7[_{3>x{þ&R<9xOmN٢I-'X4r i(yc IfGNsřSb`2bhJIF(u+TiFq9Zo1~l Gm|^ JǞ rJΊV48JJk7 *Oq4*atbԏ%Ju5JӍHEҦK[ >k>$.iͳl_ky~SIB QQJiKsu$߽]}O&o>uK] \ҮL+fQ C;c;Dn!H秈%.βIQM嘊jF']Ct{:ΕXҜ)%VNfW!|G.*L:<;bppY0W)ar):GVGO~~97;웺i7^׽ϯmWeݷ~r{܏ƪ/Y7~GFMߥmWg}ѧtֽׯ|`T..TUzK>8۳}RQqv9z~#3̽ӿWZHWZmm7Im[2yqׯ'>_^fٻWi^kn}w{-k{G=>\>-O;%v+zٴtzg֮s_+*4Zgmvܒ97'2{gw%+EO:H=_ҵoմZ4IDޝɡO|8OLg5&MnZoNm+[v}dܬvٷM]s$3g5$fTkivq묷׫O}UOyui.TgwrA]ުev֪tu7m;TZowT3^NMKZ4Z^VoevovMm8;q^FK 9ʡ9p2t_<]ƃ>g<%uzrdRr)~py7<-GRt b)QQOu)[>Glg=3_R~(%.Wmꖻk[hOZa2T3y'뎾e{^ۻ髶z?i}O[֯ox{1[I{'>#ڪ j:ͥ׃jHxi8&_bjK֎ox+ź|VYhiBpyQYkR,6tƜjܘ{6S_'79a2YÙL]Gsnu ͒s'ϯq뎆߼ݮvnɷf{kdw[+8i{Y]%;%}:׮zgWNVKg-'o[7q-n릋G]UMxsZr宊m-l[66`@ aSpp&6w[t^te:|U'JmFrfY7 bNk0Ζj0a!NxE>m#VEFmIҭ8TI#~ğ? ~[:G<1k^+x}>:Md4;[?,wn㲎Kq0\Lp`>ar<%:ASp:6Jq;+'d9Jt ucgEZ1yTRU+VUӼ;i^][ZxoKlu ^y 5Ksw5̎m dU!XQOd1l$|{}jpJ~zʔWS%%QZZ7|.&9VJ3Z!N5SR|mZ|WZxK[|a֡B狋 PZ_UR7&/<|{ˬO|%˻ML$\iv5 [ oi5;%E ^%`p\.G *3 N2u9ISVu$՜cExm᷇*?ԮJjvIBT^rr.iӡ-Bxs"bs̿88iьߕ7x'7ygipxD-Qk|7=GHAXyt'jcjJ kBrx7f?%Z\׷&ו,O*RB3Ԧ8Jj\N2Nx[YJ^K|Q4ءKY|1yZC3M$,ţw;xTwG[aڒIꨫrƜe~[;֩Rb^eXFc^SJͻ:jfJR Ks۷?=vZ%;woVW\muw^zTls=98&+Je{ZWf}:TZoz&sO_N{sVi[oT[}oE|B $qׯ$_T'd\tnV9o]u(Ȝ~v?nqQdVu>n߮Zj{zSrI3tCVһ4Ēkgu妶޻_KtGc&xg<^T]}VMidkNdZ}mi'M[#ߟϷA듼ey(ZOD";]ݮwս{3{r{όN{i֖v׮ɾw{roٹ^v5`c=220zO|yϧW{h螶I'g}ꮷI+.N rǷ?wj]_K>+v[Tߣ׫Xط^;r:'193霜n/K]ikm-/Қn~׮HO׶8?r;s4I<_mZoњ0}gǯ8y6oۯ3j׻սۏ{joUkւ:H|nwM={vto}oM4BPn.YTI۽_{KY4{]7wmn-};կ{j+W׺ݿF~~Ǿ@xn.F#IKxv.ncGu$mYjK*gJ  r59^#NvJf rOjQabjuToſkJ4oo?O~h)xukγ2IL`.xUBdm|*"URמ#Ӫ5:MJtU7<,۟RUJtUXRw^h(_c𖥩x?ç԰KЙ/o#>KG<^@ &TbXE3(bqP*uSVOޤڝԧ')p. ,˪dZN35U\P-RTzmGԧ)Po?^ O&se/y•l. =FH䌲lX)ԥON>dJZ+I]%քpbS2$*2dcpW)CjSUNHJ_)/|I}f:Rׅ\[=J643D]-˼]H.ZQ=?>b㱳氫 u|JR8RvT԰s:yJP/q>&ao 'dl8PUTO&'YA(ыjZ޹xzf[ZlF>&V@f㽷KI u8wpYJU'ZWT+TחNYJ0<*]o:&iS*~մM'Qһ\iuw[\g|H]v'o>r26ؤ94 4.,TinE*)lDSe&NQ3ZJR#p*|Jʜ);]UT+oEM~|m7ƞ) u˭>kbɥi3dm{w1_Ξ3d9v KkNqMF))^? ɸPêq*2#QjR|wIQYiBqȏlp1m׫vֽwWk=z{^<׾G^s'SwwNٽn;ۮkmCX sluߎhfVW6XMZz-}ZO_Ϲ8~r8皻m-J~-uf֪]-˦x$gӷ'T8ٸz;e'w}D+On{~y LzG{5woVO":T.(:cze]W7w[+%k4~ofvûN8<}s涋Y>s;ڋq]O_zmuO^ǁ^i;?zQ뙫ʹҺ\k[Wߚn~Wޫ\}9# u㩮I=mz5ӺڻMZukݽtKdӲ郎~3=lkeQKOUgӪ^f Ӄ~{jT%dVմwmr'{N]Z.@}G|wN+U{&uhu}^+m}=zG~7-:7t^_Vni[]Z[GuiTC>~(uM'AwkwXZ#ⳆI 6 #iU/|%^5xp \Y C4̳,4֯2\\~1ahՒLUxTisFJU#JQ=K xJ[S0YvW:Qe0V AV{ZI9ƟRdy~~#|dįχum'ƞ0\؝n-JmRhn\ ۳_WopF?V78ɰRq&ccNX\=\^59BK FMG_W5xge9eGjPȲܺjrQ<^2?sYBT?zHO==J=sTj~kh k1]i FHgȩ{Bo|=\= ZjiVU%8*jqnӜR+vY,CBj0SQP+ 8E*I^W=^; x:2w.B_ ɴeFU;}68OC)0שÞCc0Xl21f"NL®*3^rTzRdş<\Cqx,;.EdE*1ҡ8B:&[R%A|#gZχ>zm>)^GB{Yo LF:5(jԌ(Iڝ8˗].2G̅n6nV e14y!|9ө<-h{TaQo~ԣ=UJ92OE-O0.HCQӭz\m PkQ J-EP_|BsƝ}|J jLzy0-<4Z}wq\?gRqn*0`v#Stjnlno%XSsӥ˫hqiŸXJܖ; ?xU +^]&-NUӮ-4ZWrٕӭ .<5<hFU(SehԩWRITNQZm.^_1ƞ38L,7ibsU၅<]?mBdF.ubQ(Ν8Sk?%<=!kN!mWp)B6Ҋoh{yn?"`j\kvv?@J=z{W]zg$gf?;<ϚV+]WgEuͫUg'Qq;9+NTWiwo}tmkk=_E 8fZr(⣯Emuenaikh"9YcJw~imx+[tm٧\Νs98y)MGF׾kͽk$ڳk{w2fOS~C 팴ZWٵo[f=7}V4\ûNz{ Qnҍ-NJR[;5Jɴj僚hZr6@mQn8^W>Xt/nNM4zjݝn{{-;vi/;ji׿UZJi}7뻑t}9V(]unW]WokݛKMoہGN_Ǟ2MmyVu*;پ~^ɚуǨz{z,m]}tM+^dkmmk-;-n_zuQ=pzzVM-vܕwd޷^Gqm[꼴=?^ޭjjXuڣ8C(YP7cvW&Hq nsS8-d*帚fj:ʞ/-NoGcx/wq3Ͱi9a:8Ye+*< ђ-w=|^Z[3Hm-&Ia|aMnkOKlo(+u/ A,MD+oϣ,wfj\Wr3ݥ̥e/ŅGNq }<'§kkV޸7TqHe+O"Ws x{S1am8FNzUo|SI;.NWK8W!VU2\PshQXMFK:c?W/,i%[/K_Je]6ohBm3MԦnZY)6 a3!8f]B/3δp֔iW:pҚpº=ڃj p<&LaRk-/c 5j1URjQC5?^Q$5Z[}nO[$&xYm.#%ِ#m<\nǽV%R:7*:}9ƥK[ZPZYtiWJxR% ~8QQj ijݮʿֿxϏuVo2džu|{HbѴ-ئi`Hig3|?;ve᧖19NqSXZ5=&3 JJ/iRi9wo c62yKSXK .*hN)*qI]A9Y]0C[Sv\\Wq,͖?4*GT2< <5 .rݔȧ'dڒI.g+bkIS%)UJVqvQmI>Uꟶŝ;_|E߄t0既x_M=gqv-YZ%v0"%Gqaٜ*nؼVy&ؚ:r:pqU*Q})Ÿns~i!Vx 6&y~?uΥYPaaΥiRԕ9Jyf׌2źw5@dh|MKJW{ KDPcTϸ:pYq^UߝR^uKW{-)p pCd9F)EIR_ Nwԯ9ރ??ikPmuM^+{.'9LRq.Xܜ ~c_x,JUZ5z۩TrQwm$l r;!V )*8j80>o87ͤ윹ck~oư[ļ gj ^3VܛII3Oۻ^W?t*Z^{n޷~}znzt>KGTi4bwھ}DNN{~4׍z;>^׻wѳN5ggmV`A翯׽Ҿm]m4ֳ{Nv#+dGSWz,uV얝vm67oN]֯{}{Rx I?*\dҎJfݞO[M5{jշlc$;Ǟ;RZjItMwĵѯ=_U]Ii:OQsz{gv]㢿5յuսQ+kYe{-ݴ՝mBu['{ŸZk]ۭw7ufm8 v=ǿnt=s=$I׽][w%{'k﻾Wǎ?SӞO8pNVV֩|ZZjv.6N]Vy/47m`pqL{cfVvVVwV_N]˯OO;u׾8}z\vwV~_>u*|-][[}kqnmnKUi=WpIo'v^nvVPx>s\wj)$ҺR+6i5fMD~mv;릧ſ΁yqn!6%@s_xm[O>n:iY_Z\ƍJ U(MӼ,e/ǵϑio(牵 @?\xt;ָ?i\xvt&or.o O~-?x`|2p|gY. <*5jcFΌ0n\7?$!Z? ~|OX+fY6AY"\Ga2hdXn#K aZn5'<- |#ƾ+-N]:k@R^}ޫ}lɴ ye3Mq2ĘӖ`qNB1*j5:ժ*RiՍ1Q.Y=*yb+VԄZ*JNNTZ*N3a|i}akV:: Il.AC5 īe.eƘOW p4+הo*.N5jQg.k{ g$OPFPnpܹg|M?uK8_xGJJ[Z?Er.q y$kv"kyaQץXڸyM帪z]x(r.umSZZEÞƯSi*xxT9i질w.i'eJsɻH~ʫsG5~mxxOw&mGa.:Mʺ|P6P"(xXz?XU OoWQTڪ1U)ʝIxW5UQUG,=:=)ʕ:O 9^4Zom 6{ⷷ˔/įm4Cͤ h`nRy1~,1IiRҎ3kGYV~? xl9(JKv tOt;>;5'>0t[ķW!ӯlnmN r!HoGfq)ن/B7OJW('XP?q==.Lܟ&c~w^EO#(ԯ>zRQnKZL: ma|{jj:׌tK Q,Ǜk{z&9-$ID &'U)Uj +Nq0N)~MER`K5ХVʔqJ57OQU.J [_^w{W^SuCWZl^$uV҃ah:12;ISY PiC2v9#E673nO)mC1VjFeS>TFuRQ|ǯ^.ie{UzҾMӧq׮Gr8=yx/[.[&fuu$Bm#}?:ryWmn֛{=[;6=}LǷL`>96}/t[rM.M|ҿm/g8vn.EմagTնM >p2Nszӊwm{i~ȒgmV4z^nfJ?A7il|W&K4WT\{q>ܚRM2wz&~+]5$+k[~p` ^׊3K;^Wyފe5mזo8$l{^zU.vޭwzw7nNIqӿ5+6+Jmn)J^'~oqO;>'kJ;i;tݷ-E{k쓹.q#?3zVR ]lgeo[}u-}zz5]5b;O\x Dzǃ5ַQ|*nlZ2MjqfH`7%gu5%W3ԧ&]7Q啚V|oX Lh:nTùu (F|nݚ ~ƾ QMN}Z=&}[wҭ"uM>kˋxe3<5*P8Ix<:dydUqp[:9BS{ӣJ5)U{JkSQr?,,1&-ݼdJm 8z*ӋrjZN;)ihݵOh..߱#0r3q_(Q11չv8\%+^~k_|&Rm9e#)_W'J{'eAtM3c^EGO < M_kl,六 _5xY+]߭촻֚݊_^_| _ |?%WW\\4fV`qWz]} <V䋩Qʤ˫oOz~h֟ KZ왤> ^!ž#ͺN>%[K}^ Os_'3'縘br< N2*񋜰}X{ZUn$ڕJ0QRj?&rr>"&2:TqX]YhS&4rrڟ:⍎tZ [_^yڂ2lRHrBj9CZpi!`cZzj4⮣&S)7wk5%Mӧ,3UgNT7ng%vNO_Y x a&s$o*^IK!RW-WR-~Ζl+Z׵aj~Wa};g$ 5ů;a fv;,卤w*Hʦ:R_eZKYȽ2#'q.fQ–Fi>w(M򤠢*s༟:/3խ[JZ SR\%75)TSI%X'Ŀh< Zn}GԵ}Jᮦy U4cKu4q!Kğѕl,o\c!b*Unq9GF:JGXq9YU''C JJS[ *xz_,UY\)NSwjG:EѾ+_~$EnDŽ{o>,_Brh߈mXh$<Ejh)e?2d ѼN2I{RrѷZ崲̒#MSb ZI;bqYA.ZQ}?R7`y4I]O_eK壺n(;ݻ_K4l:^6~>i;^_kZ>Vw1oC?'=^[zѫۭ[Ӯz ׌\Q7;^+Vmm_7Kj n-}߳G8}i;ͻnvY^W݃z]j_zkf*L~Nj1FJM]uK[ꝝ[ u뭯Odrs'=N?LUgk6Mݾ]w뾩ݓwow{˸גzqc565[4wV+N^?wr3q˯5uݥm{ݶvu}]8k<; _{UnwQgY6dmݬktݾm}3?\/I'tޭk${_i{isv\ۯY4_.[2ZWmvV宺sN봝MM%z=Kmukgօ#㯮:ckIխܭw/@4^g=6ƬR8<TӵMw{.[ܤ;ٷn7~ksӌ{=Mc$)-UیZM-EФۺw~vv?|7XKIk0rqyW%<:xTNOoGkVʤdq4浾ot={E?|+4MJ/? ѵ;6^ 04@4_φ#T_yjꤠzT_6{虜JdArN~M˕MWBJzYo:O d\gyh>u9NP-u"Osf$,\, p2u(ԎRՠkJEIS6XRged7A^08GIՕ9Jxi:mZSu%MϚjTIqNgCb Ab`>Rvz'$JnѴ]5jkv&)VN3473ITOYE)u咳Z/e'/ݦeWW^G W)i{>cN{I#M7ŶzE+F,82lҥ')U˪(8ݾJ5a nSRKOy^/F" 2ӍJkENUV3Œ]Drj%|7g|g?WS ƞ3tCfs_.Fw柫i׺F-\qXIŇlb+ԡORIFtQnIsiv?܎0_G,~Wu,Zsνt(b0/OPJ5C.dޓ FKoŖگojmWIxSK3V1YvBI j:()JJr`Þa TA2QPT\R|39vEp Ux(SskVa5a?m=Lg,jWQjU%cۚ fx'׉OxWZuF.a_.\1,v (F!h6*VTaMkXz^qU֯VUq2F*GNITܛ# MjJ u=@+ןWMJVmGmu뵬ZW~Fݳ#q!T;3=j[;WnSj=9_j~=޷麿WWv֮M-7MMx[|2?CYIgmdZZϽyZZڽH['2rn_59YiMy edM_Gz_SENq{~GYGwtq|ϵ]";i˽wE<G$2J= ~=x<թrJI7{]o꺻[++=[GwE?O(W™4m: oQtDv>d.g|!A q1!WjTΩ>8!ҡGFPg&ײ-ɾGv?L1 ~J^ksbr}^pDB9lh3C1躏|e/iVwm]S_dq0-ZVmGs%6҉&r"9Ĩg&2],V oQ|?"^>¤(OjEJvl#С,pUX*bI3z5)S\JMՃsIma1i6V u 7F6.n$%G*YVW4Mi*+?<1wþ PVMnYԕ'4z;'gO|j~ Ǐ4 Ş+RG-oYcYa|U%#$C)x<Vڦ6Ɵ(n[OyL ֽ9VHzj5~2|Iٯ!xi:!U|(# Mʜj~\ɏo\(5'k]+Ͷb-m Ss4aeExgԥOь~*rsPqj1V?,iE9RjgWڪvӑ9JqIy+6 Ӳ7ſ x~/ӊ|]? V2!xi7IqejwRgq6at?^ӡ^*NpMFR=9Sϼ~$|5y6~fYqYЯJj1yF9d)SViL`|:xw_2MjPZ< TJHNv>$3.#*y[b8j|ю' Ztkre|Z3,s噾[Q/̰8\' K N'(JIS8Owfz7KMltN{ub9?^S2Wo[KD;^~݃V蝹ߥ^;?\*ҿ4t]>z]l[_;o} u8ޡMhO߯Qj}߿[=o-#v'OոK+y]~-mmߩ<җ,nm$kz'n{j[mH$ϡV}ϲVԶ-F익l͞^r}5 J+}Ok[ݚ9fzn__-b.\OqׁN}kUJ.Wi4~ֻjWuջi~[h\۩=;j\uwtwM,m$ޖ\׾W}{ytvOl?NJ]r+].fәGumrNd+QwVki6ַ.ms-S]֚>ڻY} |y?Ѷ벴tQgm{-fN1p{mJVvJ_յ]<5cn2ZƥKTm-Q{kuDҵ}]NƔmgz<%ʜW[I>Jm'm_}[z{3C1K=;_M]Sz'l{v޾D`־xwcVu_ Oi[x.5j ikg$W2XܻLSi'͸W8٦/eyCRTxk)B%ZM.#7s,>[S/PƬV]:_+TS2?3p/Vɩx>XE 9]bY^Mix X,/Q l4mO3)a<(St#P)Q j7QNtԥN0l7XTj՝]Ξ"jNYrqRWJZQ拌?9>;X|A#Am+7wsDs B[G|#K|q>QKf(, Bb4… ܭ^+RjG _8ƌ18lYj3 EJ/ my_+FN骉/߅֋5|<mG9tM_UMwT_zޟ]hWsLuHfhoh1\ۍq1p .{<. K SWqQu*_v ӊ+Wԭ]b3o*T _. 94%/u+O Z鷗߆ӛ]W񵷈u]RHi|A ψ,VHos#)<ʅ&'ujP)}K8a$ΝWW[K)Ud8¬MK4hXD!z0.aQJIũa>x<ҙ_fҮxO.<3iR?8\˧:n ҭmpVγY$~'8,VIZ3W]owok{u o~m;_vM{ٽS^{okiO}]_C?[z?My5&mRD/Vߓm}-?=ݒoY$Ųntzw]nץ?ϡQA=s=S>j髧fSv{Mٻ7]ov֚|%nu~[vkkvZ=[ktխ}x?F[WM8Yחg֛jՙ{u3gnQ>K'[-F3Z/[ﻵpy<׿ӞFR-ztMi[w}tk;+}U<B5wzy}l}}e7ުZo{&tI'>?ǜp/򿔖}W]ߝwmn{]MwFp϶yoL:.hdV]}5{ٍ}M+$iin[9$coӹׯa+;]NGʕmt_o$ <=vwOwmKTw}nS{[^W}uas}zssYN.ҺWM9z5gvRZګ]J*Ǽ3^$p2yWVU^p|*ZRK)νǞ5| j&~!uմ]*x5H.aצ?+Lտ#pٍ>y`kLa:Н* N8YU(PMe$f{9.qRɲq,ҾaK*f.P15*B\h(R(?n-cTxsĒOyE]z4XS xcE1RB:4M~N.UcVφb*'l%Iw,ci)SN>kbbqN'HbVe^TIs4rs/ RҼ_xTKRk :O}29нܷs(>7gEl> kO,Kt}Nl5e:*1TQM*uTh”=GxG"'yfXcn":XrC УZ5*)?=~A%=uGZ=miɴc=>ݖOhzݮ-_>}:<}3EnkjN$礷ko}5 'ۦ9[;dջi~gշ|^~^wԤx;Y4w{ڶ6t+[Zտ4PI>s'ѧ]Y;_^utۿ_k[Zuʝn?'ky?7֝SzKM^wٽfEw=Ok0\۴mKevmw9Zom;kԇ s׷o\cjwrjVݭͻ_дٵh]u{I8r8}ANҩE5n}vM>[w}[Mo}5lퟧF}u5%w'Y۷M,jE/]t-{.Mt= sgׯ~v%keJN[Gugv`ivkhWvvj[=u{n?qg8XI^J;h.g_ }5UцLS܂ U٫=}笥-v孑=]vݴwjDtOS׎=W3ZMY]-z]2W{ۣk"Ԡ{ o 4m,QH37{{ai`<+!rer~_Jp¦bƜN|ъ|ғm.V '6VXj կ^g`{j1iK97m9F_e(fOYx:׆+sZ?H^""q@tazY~xK=yR8(W0mNzTJNqp Fw>]Q G7x"u2ʝSWqgRXsR#Z2Z*7jpHNmdO:>5ާqI>PGe9/,5-rnK [9߄y ) XU}S5\𲕗R4k26ibrϞ; },NvixD#unm/"xw>1< MK~$3a1Ue_ iPnbxzP]I;~~4x3oowɭxCSӼWZnM+Zam yEҾ3FXlZ t䪺Pvn)&nT>[4UVxnʤrJ.RK[#"7!y1ut01 #yD9BtWiS唡MSkKNNm^.^e,LSIïk5QUl5u6)F_i'ykkm-yޫ]oVZze9}=*-+vݕ4nvdݷ}yА:dB:m.}>W7f߯K[w7NO\.$S oIi;W]o6;%]z-.Vz@'6z|z'%kךV['WUmmerr9_+JmڶiZk{.in=^+1mjWnܱ)IV׮O߲GoGg?z߄ .MgR&{wCoM"GU̱`rAu+*:թ9e-e^sY4?q_H a)ƏaT!?b̧/ig|,m)STz;z b_|2Cp|`e=a],X՞]`ֽ_2䖊=yKr?ۉyi WVN-)] !|"ӗx9'<K#iC.q\l],. evҌ_[s=}Zq!}%5sQ\JYF9_{;<>mkI__u9r-8~|N}9?E~+vJ2(|-s/>M{tA/׆{.#mrW?bێ~7n@:c Nk7~3~ kGcÃ><'E'w,gVw{P{vwZI *5&SmOM]kYL?L_)3Bao3q0 x @סH$fbE53 >I_"xgVUie{J]4pf?IRΛ"yz~g:V%k)E|g[ yaoϾ eǟ61q;{jNp q9'L>KJ̧)˒WT!Uj۩)I;~_5*.JRX;ܥbiSW%>ߴp}ygf)cGRAѦerCY@;0焋WIBooʒzZ}Z>34;,-or *YlwgF~ÿEj߳7)nqgz#}|*0O9[ρ9YfiO8ii9=-mO"aw m%ٵe/_߂A~m?~YA)%Aneɇ [Kx#0m )^Ŵ˖sy6# }ef+mj_q !dy&0_%R+ ^62Vana_EŦVW\~d݉CAivÝX[]u-ݣ%;K[7+#p[sI5lZqk5-%$RJmo͏kMWJtChhnMMm8Կ?.X| 74l#x=,b, ᑀ_-yu8Sʦ 6{EնIu Fs+?wu{ݽ>!j~~jxSc9REmS, +h-$.>VA*)#zS7_8v[fR%kg?(ZI'fY_% NQ}לV>|G(I]+y+ u/,Ie,IHi$rű )v H|蛞bkp+Ԟ\LeF̩eKIͮ[kϤ):2)SֆM]Bxs|mlH_P>?xP?}A'F3<_)\Tܟ[=ycջ=ӿ&éjGVeG:Y\g;+ H¤<{B\fK7eݛeUA˿~c~(kV:VW&G§>NA9Ӯz/Wd.M٫^zk&Í_it]WNO&7X㑟}19X;;ĭ4q:tW{ns2%-}Gw%41ivcu@"! #E[{ޏ>앓v ܘl)BP?5N9ԽJpR3Te62M$Zikm'ÅC})2m{,˵A T^zJ0ZWc+9%SmdKTkHe(aܝI*RS(Ԝ5bI{Wz;kΝ7Ѭz-rOE?f g we$,$liyZg{*/hyA/ze۷W(>$ۚ&qu=O/Nӫ Utۚ){B]G=7e~҅ѿ-}?XkjV{?i ?6u>oZ~ΧO ?-i Z7_m3_;G_(iZ~BkG L=}9vbuKM tJߪ]WgΒx:r/rrzc<<ոO'߯/mE/b޺6姖OdeOٶO=E׍4meYKR_*9k<c*i:34TI9]ҭ\Һ]߃MBۓT.ǥiu,L}hW,d.<϶}ggew),pX,^׃)rIMVWWMzr񰬢IEbĜY*/JWm!X>3\֨3k0,<|lX3DB!a|rC Ve;(+I9SN?{k;.*n.ғ(KU9e (ErZ{:i5%%fQĽ*=-\\y ]~#sPm* `wUN7nw+EsYZXVo IBNU$e*M-R CGχ4l+1kU|Ći$:z5{VJumZ(Nx|Vت1˳{4-9IßNxHxd|#_[Y=^X?u/.]~Zd? |g͙$,eu{N뾤SnWfݷw "u7uc֣>ؒ[4_~U{j8mj~AO}K ? Wt?>swEdX^U'!J-}??ˠׇߋI9!Ofxߞg৆ ռTH/h7~,,9M/W3Gh pZYb7g}[ƶމ]Ho9q-mwŶo[׮_Jwo'tmw#?,1A. namespace aiprovider_azureai; use core_ai\aiactions\base; use core_ai\manager; use GuzzleHttp\Psr7\Response; /** * Test Generate text provider class for azureai provider methods. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \aiprovider_azureai\provider * @covers \aiprovider_azureai\process_generate_text * @covers \aiprovider_azureai\abstract_processor */ final class process_generate_text_test extends \advanced_testcase { /** @var string A successful response in JSON format. */ protected string $responsebodyjson; /** @var manager $manager */ private manager $manager; /** @var provider The provider that will process the action. */ protected provider $provider; /** @var base The action to process. */ protected base $action; /** * Set up the test. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Load a response body from a file. $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_azureai', 'text_request_success.json')); $this->create_provider(); $this->create_action(); } /** * Create the provider object. */ private function create_provider(): void { $this->manager = \core\di::get(\core_ai\manager::class); $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableuserratelimit' => true, 'userratelimit' => 1, 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $this->provider = $this->manager->create_provider_instance( classname: '\aiprovider_azureai\provider', name: 'dummy', config: $config, ); } /** * Create the action object. * * @param int $userid The user id to use in the action. */ private function create_action(int $userid = 1): void { $this->action = new \core_ai\aiactions\generate_text( contextid: 1, userid: $userid, prompttext: 'This is a test prompt', ); } /** * Test create_request_object */ public function test_create_request_object(): void { $processor = new process_generate_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('This is a test prompt', $body->messages[1]->content); $this->assertEquals('user', $body->messages[1]->role); } /** * Test the API error response handler method. */ public function test_handle_api_error(): void { $responses = [ 500 => new Response(500, ['Content-Type' => 'application/json']), 503 => new Response(503, ['Content-Type' => 'application/json']), 401 => new Response( 401, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Invalid Authentication']]), ), 404 => new Response( 404, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'You must be a member of an organization to use the API']]), ), 429 => new Response( 429, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Rate limit reached for requests']]), ), ]; $processor = new process_generate_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_error'); foreach ($responses as $status => $response) { $result = $method->invoke($processor, $response); $this->assertEquals($status, $result['errorcode']); if ($status == 500) { $this->assertEquals('Internal Server Error', $result['errormessage']); } else if ($status == 503) { $this->assertEquals('Service Unavailable', $result['errormessage']); } else { $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']); } } } /** * Test the API success response handler method. */ public function test_handle_api_success(): void { $response = new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson ); // We're testing a private method, so we need to setup reflector magic. $processor = new process_generate_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_success'); $result = $method->invoke($processor, $response); $this->assertTrue($result['success']); $this->assertEquals('chatcmpl-9ooaXlMSUIhOkd2pfxKBgpipMynkX', $result['id']); $this->assertEquals('fp_abc28019ad', $result['fingerprint']); $this->assertStringContainsString('Sure, I\'m here to help', $result['generatedcontent']); $this->assertEquals('stop', $result['finishreason']); $this->assertEquals('12', $result['prompttokens']); $this->assertEquals('14', $result['completiontokens']); $this->assertEquals('gpt-4o-2024-05-13', $result['model']); } /** * Test query_ai_api for a successful call. */ public function test_query_ai_api_success(): void { // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'query_ai_api'); $result = $method->invoke($processor); $this->assertTrue($result['success']); $this->assertEquals('chatcmpl-9ooaXlMSUIhOkd2pfxKBgpipMynkX', $result['id']); $this->assertEquals('fp_abc28019ad', $result['fingerprint']); $this->assertStringContainsString('Sure, I\'m here to help', $result['generatedcontent']); $this->assertEquals('stop', $result['finishreason']); $this->assertEquals('12', $result['prompttokens']); $this->assertEquals('14', $result['completiontokens']); $this->assertEquals('gpt-4o-2024-05-13', $result['model']); } /** * Test prepare_response success. */ public function test_prepare_response_success(): void { $processor = new process_generate_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => true, 'id' => 'chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', 'fingerprint' => 'fp_c4e5b6fa31', 'generatedcontent' => 'Sure, here is some sample text', 'finishreason' => 'stop', 'prompttokens' => '11', 'completiontokens' => '14', 'model' => 'gpt-4o', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('generate_text', $result->get_actionname()); $this->assertEquals($response['success'], $result->get_success()); $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']); $this->assertEquals($response['model'], $result->get_response_data()['model']); } /** * Test prepare_response error. */ public function test_prepare_response_error(): void { $processor = new process_generate_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => false, 'errorcode' => 500, 'errormessage' => 'Internal server error.', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('generate_text', $result->get_actionname()); $this->assertEquals($response['errorcode'], $result->get_errorcode()); $this->assertEquals($response['errormessage'], $result->get_errormessage()); } /** * Test process method. */ public function test_process(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('generate_text', $result->get_actionname()); } /** * Test process method with error. */ public function test_process_error(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return an usuccessful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from Azure AI. $mock->append(new Response( 401, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Invalid Authentication']]), )); $processor = new process_generate_text($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('generate_text', $result->get_actionname()); $this->assertEquals(401, $result->get_errorcode()); $this->assertEquals('Invalid Authentication', $result->get_errormessage()); } /** * Test process method with user rate limiter. */ public function test_process_with_user_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the user rate limiter. $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableuserratelimit' => true, 'userratelimit' => 1, ]; $actionconfig = [ 'core_ai\\aiactions\\generate_text' => [ 'enabled' => true, 'settings' => [ 'deployment' => 'test', 'apiversion' => '2024-06-01', 'systeminstruction' => '', ], ], ]; $provider = $this->manager->create_provider_instance( classname: provider::class, name: 'dummy', config: $config, ); $provider = $this->manager->update_provider_instance( provider: $provider, actionconfig: $actionconfig, ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Case 1: User rate limit has not been reached. $this->create_action($user1->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: User rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('User rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: User rate limit has not been reached for a different user. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 4: Time window has passed, user rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } /** * Test process method with global rate limiter. */ public function test_process_with_global_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the global rate limiter. $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $actionconfig = [ 'core_ai\\aiactions\\generate_text' => [ 'enabled' => true, 'settings' => [ 'deployment' => 'test', 'apiversion' => '2024-06-01', 'systeminstruction' => '', ], ], ]; $provider = $this->manager->create_provider_instance( classname: provider::class, name: 'dummy', config: $config, ); $provider = $this->manager->update_provider_instance( provider: $provider, actionconfig: $actionconfig, ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Case 1: Global rate limit has not been reached. $this->create_action($user1->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: Global rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('Global rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: Global rate limit has been reached for a different user too. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertFalse($result->get_success()); // Case 4: Time window has passed, global rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } } PK \Vpdd%tests/process_generate_image_test.phpnu[. namespace aiprovider_azureai; use core_ai\aiactions\base; use core_ai\manager; use GuzzleHttp\Psr7\Response; /** * Test response_base Azure AI provider methods. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \aiprovider_azureai\process_generate_image */ final class process_generate_image_test extends \advanced_testcase { /** @var string A successful response in JSON format. */ protected string $responsebodyjson; /** @var manager $manager */ private manager $manager; /** @var provider The provider that will process the action. */ protected provider $provider; /** @var base The action to process. */ protected base $action; /** * Set up the test. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Load a response body from a file. $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_azureai', 'image_request_success.json')); $this->create_provider(); $this->create_action(); } /** * Create the provider object. */ private function create_provider(): void { $this->manager = \core\di::get(\core_ai\manager::class); $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableuserratelimit' => true, 'userratelimit' => 1, 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $this->provider = $this->manager->create_provider_instance( classname: '\aiprovider_azureai\provider', name: 'dummy', config: $config, ); } /** * Create the action object. * * @param int $userid The user id to use in the action. */ private function create_action(int $userid = 1): void { $this->action = new \core_ai\aiactions\generate_image( contextid: 1, userid: $userid, prompttext: 'This is a test prompt', quality: 'hd', aspectratio: 'square', numimages: 1, style: 'vivid', ); } /** * Test calculate_size. */ public function test_calculate_size(): void { $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'calculate_size'); $ratio = 'square'; $size = $method->invoke($processor, $ratio); $this->assertEquals('1024x1024', $size); $ratio = 'portrait'; $size = $method->invoke($processor, $ratio); $this->assertEquals('1024x1792', $size); $ratio = 'landscape'; $size = $method->invoke($processor, $ratio); $this->assertEquals('1792x1024', $size); } /** * Test create_request_object */ public function test_create_request_object(): void { $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $requestdata = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('This is a test prompt', $requestdata->prompt); $this->assertEquals('1', $requestdata->n); $this->assertEquals('hd', $requestdata->quality); $this->assertEquals('1024x1024', $requestdata->size); } /** * Test the API error response handler method. */ public function test_handle_api_error(): void { $responses = [ 500 => new Response(500, ['Content-Type' => 'application/json']), 503 => new Response(503, ['Content-Type' => 'application/json']), 401 => new Response(401, ['Content-Type' => 'application/json'], '{"error": {"message": "Invalid Authentication"}}'), 404 => new Response(404, ['Content-Type' => 'application/json'], '{"error": {"message": "You must be a member of an organization to use the API"}}'), 429 => new Response(429, ['Content-Type' => 'application/json'], '{"error": {"message": "Rate limit reached for requests"}}'), ]; $processor = new process_generate_image($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_error'); foreach ($responses as $status => $response) { $result = $method->invoke($processor, $response); $this->assertEquals($status, $result['errorcode']); if ($status == 500) { $this->assertEquals('Internal Server Error', $result['errormessage']); } else if ($status == 503) { $this->assertEquals('Service Unavailable', $result['errormessage']); } else { $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']); } } } /** * Test the API success response handler method. */ public function test_handle_api_success(): void { $response = new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson ); // We're testing a private method, so we need to setup reflector magic. $processor = new process_generate_image($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_success'); $result = $method->invoke($processor, $response); $this->stringContains('An image that represents the concept of a \'test\'.', $result['revisedprompt']); $this->stringContains('oaidalleapiprodscus.blob.core.windows.net', $result['sourceurl']); } /** * Test query_ai_api for a successful call. */ public function test_query_ai_api_success(): void { // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen( self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r', )), )); $this->setAdminUser(); $processor = new process_generate_image($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'query_ai_api'); $result = $method->invoke($processor); $this->stringContains('An image that represents the concept of a \'test\'.', $result['revisedprompt']); $this->stringContains('oaidalleapiprodscus.blob.core.windows.net', $result['sourceurl']); } /** * Test prepare_response success. */ public function test_prepare_response_success(): void { $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => true, 'revisedprompt' => 'An image that represents the concept of a \'test\'.', 'imageurl' => 'oaidalleapiprodscus.blob.core.windows.net', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('generate_image', $result->get_actionname()); $this->assertEquals($response['success'], $result->get_success()); $this->assertEquals($response['revisedprompt'], $result->get_response_data()['revisedprompt']); } /** * Test prepare_response error. */ public function test_prepare_response_error(): void { $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => false, 'errorcode' => 500, 'errormessage' => 'Internal server error.', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('generate_image', $result->get_actionname()); $this->assertEquals($response['errorcode'], $result->get_errorcode()); $this->assertEquals($response['errormessage'], $result->get_errormessage()); } /** * Test url_to_file. */ public function test_url_to_file(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'url_to_file'); $contextid = 1; $url = $this->getExternalTestFileUrl('/test.jpg', false); $fileobj = $method->invoke($processor, $contextid, $url); $this->assertEquals('user', $fileobj->get_component()); $this->assertEquals('draft', $fileobj->get_filearea()); } /** * Test process. */ public function test_process(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); $url = 'https://example.com/test.jpg'; // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); // Create a request object. $contextid = 1; $userid = 1; $prompttext = 'This is a test prompt'; $aspectratio = 'square'; $quality = 'hd'; $numimages = 1; $style = 'vivid'; $this->action = new \core_ai\aiactions\generate_image( contextid: $contextid, userid: $userid, prompttext: $prompttext, quality: $quality, aspectratio: $aspectratio, numimages: $numimages, style: $style, ); $processor = new process_generate_image($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('generate_image', $result->get_actionname()); $this->assertEquals('An image that represents the concept of a \'test\'.', $result->get_response_data()['revisedprompt']); $this->assertEquals($url, $result->get_response_data()['sourceurl']); } /** * Test process method with error. */ public function test_process_error(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from Azure AI. $mock->append(new Response( 401, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Invalid Authentication']]), )); $processor = new process_generate_image($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('generate_image', $result->get_actionname()); $this->assertEquals(401, $result->get_errorcode()); $this->assertEquals('Invalid Authentication', $result->get_errormessage()); } /** * Test process method with user rate limiter. */ public function test_process_with_user_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the user rate limiter. $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableuserratelimit' => true, 'userratelimit' => 1, ]; $actionconfig = [ 'core_ai\\aiactions\\generate_image' => [ 'enabled' => true, 'settings' => [ 'deployment' => 'test', 'apiversion' => '2024-06-01', ], ], ]; $provider = $this->manager->create_provider_instance( classname: provider::class, name: 'dummy', config: $config, ); $provider = $this->manager->update_provider_instance( provider: $provider, actionconfig: $actionconfig, ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); $url = 'https://example.com/test.jpg'; // Case 1: User rate limit has not been reached. $this->create_action($user1->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: User rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); $this->create_action($user1->id); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('User rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: User rate limit has not been reached for a different user. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 4: Time window has passed, user rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); $this->create_action($user1->id); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } /** * Test process method with global rate limiter. */ public function test_process_with_global_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the global rate limiter. $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $actionconfig = [ 'core_ai\\aiactions\\generate_image' => [ 'enabled' => true, 'settings' => [ 'deployment' => 'test', 'apiversion' => '2024-06-01', ], ], ]; $provider = $this->manager->create_provider_instance( classname: provider::class, name: 'dummy', config: $config, ); $provider = $this->manager->update_provider_instance( provider: $provider, actionconfig: $actionconfig, ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); $url = 'https://example.com/test.jpg'; // Case 1: Global rate limit has not been reached. $this->create_action($user1->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: Global rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); $this->create_action($user1->id); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('Global rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: Global rate limit has been reached for a different user too. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertFalse($result->get_success()); // Case 4: Time window has passed, global rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_azureai', 'test.jpg'), 'r')), )); $this->create_action($user1->id); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } } PK \F JJ%tests/process_summarise_text_test.phpnu[. namespace aiprovider_azureai; use core_ai\aiactions\base; use core_ai\manager; use GuzzleHttp\Psr7\Response; /** * Test Generate text provider class for Azure AI provider methods. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \aiprovider_azureai\provider * @covers \aiprovider_azureai\process_summarise_text * @covers \aiprovider_azureai\abstract_processor */ final class process_summarise_text_test extends \advanced_testcase { /** @var string A successful response in JSON format. */ protected string $responsebodyjson; /** @var manager $manager */ private manager $manager; /** @var provider The provider that will process the action. */ protected provider $provider; /** @var base The action to process. */ protected base $action; /** * Set up the test. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Load a response body from a file. $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_azureai', 'text_request_success.json')); $this->create_provider(); $this->create_action(); } /** * Create the provider object. */ private function create_provider(): void { $this->manager = \core\di::get(\core_ai\manager::class); $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableuserratelimit' => true, 'userratelimit' => 1, 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $this->provider = $this->manager->create_provider_instance( classname: '\aiprovider_azureai\provider', name: 'dummy', config: $config, ); } /** * Create the action object. * * @param int $userid The user id to use in the action. */ private function create_action(int $userid = 1): void { $this->action = new \core_ai\aiactions\summarise_text( contextid: 1, userid: $userid, prompttext: 'This is a test prompt', ); } /** * Test create_request_object */ public function test_create_request_object(): void { $processor = new process_summarise_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('system', $body->messages[0]->role); $this->assertEquals(get_string('action_summarise_text_instruction', 'core_ai'), $body->messages[0]->content); $this->assertEquals('This is a test prompt', $body->messages[1]->content); $this->assertEquals('user', $body->messages[1]->role); } /** * Test the API error response handler method. */ public function test_handle_api_error(): void { $responses = [ 500 => new Response(500, ['Content-Type' => 'application/json']), 503 => new Response(503, ['Content-Type' => 'application/json']), 401 => new Response(401, ['Content-Type' => 'application/json'], '{"error": {"message": "Invalid Authentication"}}'), 404 => new Response(404, ['Content-Type' => 'application/json'], '{"error": {"message": "You must be a member of an organization to use the API"}}'), 429 => new Response(429, ['Content-Type' => 'application/json'], '{"error": {"message": "Rate limit reached for requests"}}'), ]; $processor = new process_summarise_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_error'); foreach ($responses as $status => $response) { $result = $method->invoke($processor, $response); $this->assertEquals($status, $result['errorcode']); if ($status == 500) { $this->assertEquals('Internal Server Error', $result['errormessage']); } else if ($status == 503) { $this->assertEquals('Service Unavailable', $result['errormessage']); } else { $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']); } } } /** * Test the API success response handler method. */ public function test_handle_api_success(): void { $response = new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson ); // We're testing a private method, so we need to set up reflector magic. $processor = new process_summarise_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_success'); $result = $method->invoke($processor, $response); $this->assertTrue($result['success']); $this->assertEquals('chatcmpl-9ooaXlMSUIhOkd2pfxKBgpipMynkX', $result['id']); $this->assertEquals('fp_abc28019ad', $result['fingerprint']); $this->assertStringContainsString('Sure, I\'m here to help!', $result['generatedcontent']); $this->assertEquals('stop', $result['finishreason']); $this->assertEquals('12', $result['prompttokens']); $this->assertEquals('14', $result['completiontokens']); $this->assertEquals('gpt-4o-2024-05-13', $result['model']); } /** * Test query_ai_api for a successful call. */ public function test_query_ai_api_success(): void { // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'query_ai_api'); $result = $method->invoke($processor); $this->assertTrue($result['success']); $this->assertEquals('chatcmpl-9ooaXlMSUIhOkd2pfxKBgpipMynkX', $result['id']); $this->assertEquals('fp_abc28019ad', $result['fingerprint']); $this->assertStringContainsString('Sure, I\'m here to help!', $result['generatedcontent']); $this->assertEquals('stop', $result['finishreason']); $this->assertEquals('12', $result['prompttokens']); $this->assertEquals('14', $result['completiontokens']); $this->assertEquals('gpt-4o-2024-05-13', $result['model']); } /** * Test prepare_response success. */ public function test_prepare_response_success(): void { $processor = new process_summarise_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => true, 'id' => 'chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', 'fingerprint' => 'fp_c4e5b6fa31', 'generatedcontent' => 'Sure, here is some sample text', 'finishreason' => 'stop', 'prompttokens' => '11', 'completiontokens' => '568', 'model' => 'gpt-4o', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('summarise_text', $result->get_actionname()); $this->assertEquals($response['success'], $result->get_success()); $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']); $this->assertEquals($response['model'], $result->get_response_data()['model']); } /** * Test prepare_response error. */ public function test_prepare_response_error(): void { $processor = new process_summarise_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => false, 'errorcode' => 500, 'errormessage' => 'Internal server error.', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('summarise_text', $result->get_actionname()); $this->assertEquals($response['errorcode'], $result->get_errorcode()); $this->assertEquals($response['errormessage'], $result->get_errormessage()); } /** * Test process method. */ public function test_process(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('summarise_text', $result->get_actionname()); } /** * Test process method with error. */ public function test_process_error(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return an unsuccessful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from AzureAI. $mock->append(new Response( 401, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Invalid Authentication']]), )); $processor = new process_summarise_text($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('summarise_text', $result->get_actionname()); $this->assertEquals(401, $result->get_errorcode()); $this->assertEquals('Invalid Authentication', $result->get_errormessage()); } /** * Test process method with user rate limiter. */ public function test_process_with_user_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the user rate limiter. $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableuserratelimit' => true, 'userratelimit' => 1, ]; $actionconfig = [ 'core_ai\\aiactions\\summarise_text' => [ 'enabled' => true, 'settings' => [ 'deployment' => 'test', 'apiversion' => '2024-06-01', 'systeminstruction' => '', ], ], ]; $provider = $this->manager->create_provider_instance( classname: provider::class, name: 'dummy', config: $config, ); $provider = $this->manager->update_provider_instance( provider: $provider, actionconfig: $actionconfig, ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Case 1: User rate limit has not been reached. $this->create_action($user1->id); // The response from Azure I. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: User rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('User rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: User rate limit has not been reached for a different user. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 4: Time window has passed, user rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } /** * Test process method with global rate limiter. */ public function test_process_with_global_rate_limiter(): void { $this->resetAfterTest(); // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the global rate limiter. $config = [ 'apikey' => '123', 'endpoint' => 'https://api.example.com', 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $actionconfig = [ 'core_ai\\aiactions\\summarise_text' => [ 'enabled' => true, 'settings' => [ 'deployment' => 'test', 'apiversion' => '2024-06-01', 'systeminstruction' => '', ], ], ]; $provider = $this->manager->create_provider_instance( classname: provider::class, name: 'dummy', config: $config, ); $provider = $this->manager->update_provider_instance( provider: $provider, actionconfig: $actionconfig, ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Case 1: Global rate limit has not been reached. $this->create_action($user1->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: Global rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('Global rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: Global rate limit has been reached for a different user too. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertFalse($result->get_success()); // Case 4: Time window has passed, global rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from Azure AI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } } PK \2êtests/provider_test.phpnu[. namespace aiprovider_azureai; use core_ai\manager; /** * Test Azure AI provider methods. * * @package aiprovider_azureai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \aiprovider_azureai\provider */ final class provider_test extends \advanced_testcase { /** @var manager $manager */ private manager $manager; /** @var provider $provider */ private provider $provider; /** * Overriding setUp() function to always reset after tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Create the provider instance. $this->manager = \core\di::get(\core_ai\manager::class); $config = ['data' => 'goeshere']; $this->provider = $this->manager->create_provider_instance( classname: '\aiprovider_azureai\provider', name: 'dummy', config: $config, ); } /** * Test get_action_list */ public function test_get_action_list(): void { $actionlist = $this->provider->get_action_list(); $this->assertIsArray($actionlist); $this->assertEquals(4, count($actionlist)); $this->assertContains(\core_ai\aiactions\generate_text::class, $actionlist); $this->assertContains(\core_ai\aiactions\generate_image::class, $actionlist); $this->assertContains(\core_ai\aiactions\summarise_text::class, $actionlist); $this->assertContains(\core_ai\aiactions\explain_text::class, $actionlist); } /** * Test generate_userid. */ public function test_generate_userid(): void { $userid = $this->provider->generate_userid(1); // Assert that the generated userid is a string of proper length. $this->assertIsString($userid); $this->assertEquals(64, strlen($userid)); } /** * Test is_request_allowed. */ public function test_is_request_allowed(): void { // Create the provider instance. $config = [ 'enableuserratelimit' => true, 'userratelimit' => 3, 'enableglobalratelimit' => true, 'globalratelimit' => 5, ]; /** @var provider $provider */ $provider = $this->manager->create_provider_instance( classname: provider::class, name: 'dummy', config: $config, ); $contextid = 1; $userid = 1; $prompttext = 'This is a test prompt'; $aspectratio = 'square'; $quality = 'hd'; $numimages = 1; $style = 'vivid'; $action = new \core_ai\aiactions\generate_image( contextid: $contextid, userid: $userid, prompttext: $prompttext, quality: $quality, aspectratio: $aspectratio, numimages: $numimages, style: $style ); // Make 3 requests, all should be allowed. for ($i = 0; $i < 3; $i++) { $this->assertTrue($provider->is_request_allowed($action)); } // The 4th request for the same user should be denied. $result = $provider->is_request_allowed($action); $this->assertFalse($result['success']); $this->assertEquals('User rate limit exceeded', $result['errormessage']); // Change user id to make a request for a different user, should pass (4 requests for global rate). $action = new \core_ai\aiactions\generate_image( contextid: $contextid, userid: 2, prompttext: $prompttext, quality: $quality, aspectratio: $aspectratio, numimages: $numimages, style: $style); $this->assertTrue($provider->is_request_allowed($action)); // Make a 5th request for the global rate limit, it should be allowed. $this->assertTrue($provider->is_request_allowed($action)); // The 6th request should be denied. $result = $provider->is_request_allowed($action); $this->assertFalse($result['success']); $this->assertEquals('Global rate limit exceeded', $result['errormessage']); } /** * Test is_provider_configured. */ public function test_is_provider_configured(): void { // No configured values. $this->assertFalse($this->provider->is_provider_configured()); // Partially configured values. $updatedprovider = $this->manager->update_provider_instance( provider: $this->provider, config: ['apikey' => '123'], ); $this->assertFalse($updatedprovider->is_provider_configured()); // Properly configured values. $updatedprovider = $this->manager->update_provider_instance( provider: $this->provider, config: [ 'apikey' => '123', 'endpoint' => 'abc', ], ); $this->assertTrue($updatedprovider->is_provider_configured()); } } PK \Vd db/hooks.phpnu[PK \6͛_lang/en/deprecated.txtnu[PK \M llang/en/aiprovider_azureai.phpnu[PK \!cNGG version.phpnu[PK \{Z Z +E"classes/form/action_generate_image_form.phpnu[PK \.t6*.classes/form/action_generate_text_form.phpnu[PK \ūA  =classes/privacy/provider.phpnu[PK \hІ"/Iclasses/process_generate_image.phpnu[PK \"aclasses/process_summarise_text.phpnu[PK \in?eclasses/hook_listener.phpnu[PK \7Hnclasses/abstract_processor.phpnu[PK \nF F classes/provider.phpnu[PK \mR* classes/process_explain_text.phpnu[PK \)!Qclasses/process_generate_text.phpnu[PK \((tests/fixtures/text_request_success.jsonnu[PK \]>6)tests/fixtures/image_request_success.jsonnu[PK \:Dr44tests/fixtures/test.jpgnu[PK \DwJwJ$Etests/process_generate_text_test.phpnu[PK \Vpdd%ˏtests/process_generate_image_test.phpnu[PK \F JJ%tests/process_summarise_text_test.phpnu[PK \2ê?tests/provider_test.phpnu[PKV