mirror of
https://github.com/moodle/moodle.git
synced 2025-08-09 02:46:40 +02:00
MDL-82411 AI: Provider Plugin - Azure AI
Add an AI provider plugin for Microsoft Azure AI. The Azure AI provider supports generating images, as well as generating and summarising text.
This commit is contained in:
parent
f6141a67d8
commit
75382a1e92
18 changed files with 2650 additions and 5 deletions
139
ai/provider/azureai/classes/abstract_processor.php
Normal file
139
ai/provider/azureai/classes/abstract_processor.php
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 <matt.porritt@moodle.com>
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
abstract protected function get_deployment_name(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the api version to use.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
abstract protected function get_api_version(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
77
ai/provider/azureai/classes/privacy/provider.php
Normal file
77
ai/provider/azureai/classes/privacy/provider.php
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 <matt.porritt@moodle.com>
|
||||||
|
* @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) {
|
||||||
|
}
|
||||||
|
}
|
175
ai/provider/azureai/classes/process_generate_image.php
Normal file
175
ai/provider/azureai/classes/process_generate_image.php
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 <matt.porritt@moodle.com>
|
||||||
|
* @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(get_config('aiprovider_azureai', 'endpoint'), '/')
|
||||||
|
. '/openai/deployments/'
|
||||||
|
. $this->get_deployment_name()
|
||||||
|
. '/images/generations?api-version='
|
||||||
|
. $this->get_api_version();
|
||||||
|
|
||||||
|
return new Uri($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
|
protected function get_deployment_name(): string {
|
||||||
|
return get_config('aiprovider_azureai', 'action_generate_image_deployment');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
|
protected function get_api_version(): string {
|
||||||
|
return get_config('aiprovider_azureai', 'action_generate_image_apiversion');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\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: '',
|
||||||
|
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,
|
||||||
|
]),
|
||||||
|
headers: [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\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));
|
||||||
|
}
|
||||||
|
}
|
111
ai/provider/azureai/classes/process_generate_text.php
Normal file
111
ai/provider/azureai/classes/process_generate_text.php
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 <matt.porritt@moodle.com>
|
||||||
|
* @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(get_config('aiprovider_azureai', 'endpoint'), '/')
|
||||||
|
. '/openai/deployments/'
|
||||||
|
. $this->get_deployment_name()
|
||||||
|
. '/chat/completions?api-version='
|
||||||
|
. $this->get_api_version();
|
||||||
|
|
||||||
|
return new Uri($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
|
protected function get_deployment_name(): string {
|
||||||
|
return get_config('aiprovider_azureai', 'action_generate_text_deployment');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
|
protected function get_api_version(): string {
|
||||||
|
return get_config('aiprovider_azureai', 'action_generate_text_apiversion');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
|
protected function get_system_instruction(): string {
|
||||||
|
return get_config('aiprovider_azureai', 'action_generate_text_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: '',
|
||||||
|
body: json_encode($requestobj),
|
||||||
|
headers: [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
41
ai/provider/azureai/classes/process_summarise_text.php
Normal file
41
ai/provider/azureai/classes/process_summarise_text.php
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace aiprovider_azureai;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class process text summarisation.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
class process_summarise_text extends process_generate_text {
|
||||||
|
#[\Override]
|
||||||
|
protected function get_deployment_name(): string {
|
||||||
|
return get_config('aiprovider_azureai', 'action_summarise_text_deployment');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
|
protected function get_api_version(): string {
|
||||||
|
return get_config('aiprovider_azureai', 'action_summarise_text_apiversion');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
|
protected function get_system_instruction(): string {
|
||||||
|
return get_config('aiprovider_azureai', 'action_summarise_text_systeminstruction');
|
||||||
|
}
|
||||||
|
}
|
193
ai/provider/azureai/classes/provider.php
Normal file
193
ai/provider/azureai/classes/provider.php
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace aiprovider_azureai;
|
||||||
|
|
||||||
|
use core_ai\aiactions;
|
||||||
|
use core_ai\rate_limiter;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class provider.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
class provider extends \core_ai\provider {
|
||||||
|
/** @var string The Azure AI API key. */
|
||||||
|
private string $apikey;
|
||||||
|
|
||||||
|
/** @var string The Azure AI API endpoint, is different for each organisation. */
|
||||||
|
public string $apiendpoint;
|
||||||
|
|
||||||
|
/** @var bool Is global rate limiting for the API enabled. */
|
||||||
|
private bool $enableglobalratelimit;
|
||||||
|
|
||||||
|
/** @var int The global rate limit. */
|
||||||
|
private int $globalratelimit;
|
||||||
|
|
||||||
|
/** @var bool Is user rate limiting for the API enabled */
|
||||||
|
private bool $enableuserratelimit;
|
||||||
|
|
||||||
|
/** @var int The user rate limit. */
|
||||||
|
private int $userratelimit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
// Get api key from config.
|
||||||
|
$this->apikey = get_config('aiprovider_azureai', 'apikey');
|
||||||
|
// Get api endpoint url id from config.
|
||||||
|
$this->apiendpoint = get_config('aiprovider_azureai', 'endpoint');
|
||||||
|
// Get global rate limit from config.
|
||||||
|
$this->enableglobalratelimit = get_config('aiprovider_azureai', 'enableglobalratelimit');
|
||||||
|
$this->globalratelimit = get_config('aiprovider_azureai', 'globalratelimit');
|
||||||
|
// Get user rate limit from config.
|
||||||
|
$this->enableuserratelimit = get_config('aiprovider_azureai', 'enableuserratelimit');
|
||||||
|
$this->userratelimit = get_config('aiprovider_azureai', 'userratelimit');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of actions that this provider supports.
|
||||||
|
*
|
||||||
|
* @return array An array of action class names.
|
||||||
|
*/
|
||||||
|
public function get_action_list(): array {
|
||||||
|
return [
|
||||||
|
\core_ai\aiactions\generate_text::class,
|
||||||
|
\core_ai\aiactions\generate_image::class,
|
||||||
|
\core_ai\aiactions\summarise_text::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a user id.
|
||||||
|
* This is a hash of the site id and user id,
|
||||||
|
* this means we can determine who made the request
|
||||||
|
* but don't pass any personal data to AzureAI.
|
||||||
|
*
|
||||||
|
* @param string $userid The user id.
|
||||||
|
* @return string The generated user id.
|
||||||
|
*/
|
||||||
|
public function generate_userid(string $userid): string {
|
||||||
|
global $CFG;
|
||||||
|
return hash('sha256', $CFG->siteidentifier . $userid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a request to add any headers required by the provider.
|
||||||
|
*
|
||||||
|
* @param \Psr\Http\Message\RequestInterface $request
|
||||||
|
* @return \Psr\Http\Message\RequestInterface
|
||||||
|
*/
|
||||||
|
public function add_authentication_headers(RequestInterface $request): RequestInterface {
|
||||||
|
return $request
|
||||||
|
->withAddedHeader('api-key', $this->apikey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is allowed by the rate limiter.
|
||||||
|
*
|
||||||
|
* @param aiactions\base $action The action to check.
|
||||||
|
* @return array|bool True on success, array of error details on failure.
|
||||||
|
*/
|
||||||
|
public function is_request_allowed(aiactions\base $action): array|bool {
|
||||||
|
$ratelimiter = \core\di::get(rate_limiter::class);
|
||||||
|
$component = \core\component::get_component_from_classname(get_class($this));
|
||||||
|
|
||||||
|
// Check the user rate limit.
|
||||||
|
if ($this->enableuserratelimit) {
|
||||||
|
if (!$ratelimiter->check_user_rate_limit(
|
||||||
|
component: $component,
|
||||||
|
ratelimit: $this->userratelimit,
|
||||||
|
userid: $action->get_configuration('userid')
|
||||||
|
)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'errorcode' => 429,
|
||||||
|
'errormessage' => 'User rate limit exceeded',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the global rate limit.
|
||||||
|
if ($this->enableglobalratelimit) {
|
||||||
|
if (!$ratelimiter->check_global_rate_limit(
|
||||||
|
component: $component,
|
||||||
|
ratelimit: $this->globalratelimit)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'errorcode' => 429,
|
||||||
|
'errormessage' => 'Global rate limit exceeded',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get any action settings for this provider.
|
||||||
|
*
|
||||||
|
* @param string $action The action class name.
|
||||||
|
* @param \admin_root $ADMIN The admin root object.
|
||||||
|
* @param string $section The section name.
|
||||||
|
* @param bool $hassiteconfig Whether the current user has moodle/site:config capability.
|
||||||
|
* @return array An array of settings.
|
||||||
|
*/
|
||||||
|
public function get_action_settings(
|
||||||
|
string $action,
|
||||||
|
\admin_root $ADMIN,
|
||||||
|
string $section,
|
||||||
|
bool $hassiteconfig
|
||||||
|
): array {
|
||||||
|
$actionname = substr($action, (strrpos($action, '\\') + 1));
|
||||||
|
$settings = [];
|
||||||
|
|
||||||
|
// Add API deployment name.
|
||||||
|
$settings[] = new \admin_setting_configtext(
|
||||||
|
"aiprovider_azureai/action_{$actionname}_deployment",
|
||||||
|
new \lang_string("action_deployment", 'aiprovider_azureai'),
|
||||||
|
new \lang_string("action_deployment_desc", 'aiprovider_azureai'),
|
||||||
|
'',
|
||||||
|
PARAM_ALPHANUMEXT,
|
||||||
|
);
|
||||||
|
// Add API version.
|
||||||
|
$settings[] = new \admin_setting_configtext(
|
||||||
|
"aiprovider_azureai/action_{$actionname}_apiversion",
|
||||||
|
new \lang_string("action_apiversion", 'aiprovider_azureai'),
|
||||||
|
new \lang_string("action_apiversion_desc", 'aiprovider_azureai'),
|
||||||
|
'2024-06-01',
|
||||||
|
PARAM_ALPHANUMEXT,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($actionname === 'generate_text' || $actionname === 'summarise_text') {
|
||||||
|
// Add system instruction settings.
|
||||||
|
$settings[] = new \admin_setting_configtextarea(
|
||||||
|
"aiprovider_azureai/action_{$actionname}_systeminstruction",
|
||||||
|
new \lang_string("action_systeminstruction", 'aiprovider_azureai'),
|
||||||
|
new \lang_string("action_systeminstruction_desc", 'aiprovider_azureai'),
|
||||||
|
$action::get_system_instruction(),
|
||||||
|
PARAM_TEXT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
51
ai/provider/azureai/lang/en/aiprovider_azureai.php
Normal file
51
ai/provider/azureai/lang/en/aiprovider_azureai.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strings for component aiprovider_azureai, language 'en'.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
$string['action_apiversion'] = 'Azure AI API version';
|
||||||
|
$string['action_apiversion_desc'] = 'Enter the version number for your Azure AI API.';
|
||||||
|
$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'] = 'The instruction is used provided along with the user request for this action. It provides information to the AI model on how to generate the response.';
|
||||||
|
$string['apikey'] = 'Azure AI API key';
|
||||||
|
$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'] = 'Enable global rate limiting';
|
||||||
|
$string['enableglobalratelimit_desc'] = 'Enable global rate limiting for the Azure AI API provider.';
|
||||||
|
$string['enableuserratelimit'] = 'Enable user rate limiting';
|
||||||
|
$string['enableuserratelimit_desc'] = 'Enable user rate limiting for the Azure AI API provider.';
|
||||||
|
$string['endpoint'] = 'Azure AI API endpoint';
|
||||||
|
$string['endpoint_desc'] = 'Enter the endpoint URL for your Azure AI API. In the form of: https://YOUR_RESOURCE_NAME.azureai.azure.com/azureai/deployments';
|
||||||
|
$string['globalratelimit'] = 'Global rate limit';
|
||||||
|
$string['globalratelimit_desc'] = 'Set the number of requests per hour allowed for the global rate limit.';
|
||||||
|
$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'] = 'The number of images used in the response. When generating images.';
|
||||||
|
$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.';
|
||||||
|
$string['userratelimit'] = 'User rate limit';
|
||||||
|
$string['userratelimit_desc'] = 'Set the number of requests per hour allowed for the user rate limit.';
|
96
ai/provider/azureai/settings.php
Normal file
96
ai/provider/azureai/settings.php
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - https://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin administration pages are defined here.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
use core_ai\admin\admin_settingspage_provider;
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
if ($hassiteconfig) {
|
||||||
|
// Provider specific settings.
|
||||||
|
$settings = new admin_settingspage_provider(
|
||||||
|
'aiprovider_azureai',
|
||||||
|
new lang_string('pluginname', 'aiprovider_azureai'),
|
||||||
|
'moodle/site:config',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$settings->add(new admin_setting_heading(
|
||||||
|
'aiprovider_azureai/general',
|
||||||
|
new lang_string('providersettings', 'core_ai'),
|
||||||
|
new lang_string('providersettings_desc', 'core_ai')
|
||||||
|
));
|
||||||
|
|
||||||
|
// Setting to store AzureAI API key.
|
||||||
|
$settings->add(new admin_setting_configpasswordunmask(
|
||||||
|
'aiprovider_azureai/apikey',
|
||||||
|
new lang_string('apikey', 'aiprovider_azureai'),
|
||||||
|
new lang_string('apikey_desc', 'aiprovider_azureai'),
|
||||||
|
'',
|
||||||
|
));
|
||||||
|
|
||||||
|
// Setting to store AzureAI endpoint URL.
|
||||||
|
$settings->add(new admin_setting_configtext(
|
||||||
|
'aiprovider_azureai/endpoint',
|
||||||
|
new lang_string('endpoint', 'aiprovider_azureai'),
|
||||||
|
new lang_string('endpoint_desc', 'aiprovider_azureai'),
|
||||||
|
'',
|
||||||
|
PARAM_URL
|
||||||
|
));
|
||||||
|
|
||||||
|
// Setting to enable/disable global rate limiting.
|
||||||
|
$settings->add(new admin_setting_configcheckbox('aiprovider_azureai/enableglobalratelimit',
|
||||||
|
new lang_string('enableglobalratelimit', 'aiprovider_azureai'),
|
||||||
|
new lang_string('enableglobalratelimit_desc', 'aiprovider_azureai'),
|
||||||
|
0
|
||||||
|
));
|
||||||
|
|
||||||
|
// Setting to set how many requests per hour are allowed for the global rate limit.
|
||||||
|
// Should only be enabled when global rate limiting is enabled.
|
||||||
|
$settings->add(new admin_setting_configtext(
|
||||||
|
'aiprovider_azureai/globalratelimit',
|
||||||
|
new lang_string('globalratelimit', 'aiprovider_azureai'),
|
||||||
|
new lang_string('globalratelimit_desc', 'aiprovider_azureai'),
|
||||||
|
100,
|
||||||
|
PARAM_INT
|
||||||
|
));
|
||||||
|
$settings->hide_if('aiprovider_azureai/globalratelimit', 'aiprovider_azureai/enableglobalratelimit', 'eq', 0);
|
||||||
|
|
||||||
|
// Setting to enable/disable user rate limiting.
|
||||||
|
$settings->add(new admin_setting_configcheckbox(
|
||||||
|
'aiprovider_azureai/enableuserratelimit',
|
||||||
|
new lang_string('enableuserratelimit', 'aiprovider_azureai'),
|
||||||
|
new lang_string('enableuserratelimit_desc', 'aiprovider_azureai'),
|
||||||
|
0
|
||||||
|
));
|
||||||
|
|
||||||
|
// Setting to set how many requests per hour are allowed for the user rate limit.
|
||||||
|
// Should only be enabled when user rate limiting is enabled.
|
||||||
|
$settings->add(new admin_setting_configtext(
|
||||||
|
'aiprovider_azureai/userratelimit',
|
||||||
|
new lang_string('userratelimit', 'aiprovider_azureai'),
|
||||||
|
new lang_string('userratelimit_desc', 'aiprovider_azureai'),
|
||||||
|
10,
|
||||||
|
PARAM_INT));
|
||||||
|
$settings->hide_if('aiprovider_azureai/userratelimit', 'aiprovider_azureai/enableuserratelimit', 'eq', 0);
|
||||||
|
}
|
9
ai/provider/azureai/tests/fixtures/image_request_success.json
vendored
Normal file
9
ai/provider/azureai/tests/fixtures/image_request_success.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
ai/provider/azureai/tests/fixtures/test.jpg
vendored
Normal file
BIN
ai/provider/azureai/tests/fixtures/test.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
68
ai/provider/azureai/tests/fixtures/text_request_success.json
vendored
Normal file
68
ai/provider/azureai/tests/fixtures/text_request_success.json
vendored
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
646
ai/provider/azureai/tests/process_generate_image_test.php
Normal file
646
ai/provider/azureai/tests/process_generate_image_test.php
Normal file
|
@ -0,0 +1,646 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace aiprovider_azureai;
|
||||||
|
|
||||||
|
use core_ai\aiactions\base;
|
||||||
|
use core_ai\provider;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test response_base Azure AI provider methods.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
* @covers \core_ai\provider\azureai
|
||||||
|
*/
|
||||||
|
final class process_generate_image_test extends \advanced_testcase {
|
||||||
|
/** @var string A successful response in JSON format. */
|
||||||
|
protected string $responsebodyjson;
|
||||||
|
|
||||||
|
/** @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();
|
||||||
|
// 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->provider = new \aiprovider_azureai\provider();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// Create a request object.
|
||||||
|
$requestobj = new \stdClass();
|
||||||
|
$requestobj->prompt = 'generate a test image';
|
||||||
|
$requestobj->model = 'awesome-ai-3';
|
||||||
|
$requestobj->n = '3';
|
||||||
|
$requestobj->quality = 'hd';
|
||||||
|
$requestobj->response_format = 'url;';
|
||||||
|
$requestobj->size = '1024x1024';
|
||||||
|
$requestobj->style = 'vivid';
|
||||||
|
$requestobj->user = 't3464h89dftjltestudfaser';
|
||||||
|
|
||||||
|
$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 {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
// 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 {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
// 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 {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
// 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 {
|
||||||
|
$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 user rate limiter.
|
||||||
|
set_config('enableuserratelimit', 1, 'aiprovider_azureai');
|
||||||
|
set_config('userratelimit', 1, 'aiprovider_azureai');
|
||||||
|
|
||||||
|
// 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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_generate_image($this->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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_generate_image($this->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.
|
||||||
|
set_config('enableglobalratelimit', 1, 'aiprovider_azureai');
|
||||||
|
set_config('globalratelimit', 1, 'aiprovider_azureai');
|
||||||
|
|
||||||
|
// 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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_generate_image($this->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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_generate_image($this->provider, $this->action);
|
||||||
|
$result = $processor->process();
|
||||||
|
$this->assertTrue($result->get_success());
|
||||||
|
}
|
||||||
|
}
|
452
ai/provider/azureai/tests/process_generate_text_test.php
Normal file
452
ai/provider/azureai/tests/process_generate_text_test.php
Normal file
|
@ -0,0 +1,452 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace aiprovider_azureai;
|
||||||
|
|
||||||
|
use aiprovider_azureai\process_generate_text;
|
||||||
|
use core_ai\aiactions\base;
|
||||||
|
use core_ai\aiactions\generate_text;
|
||||||
|
use core_ai\provider;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Generate text provider class for azureai provider methods.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @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 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();
|
||||||
|
// 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->provider = new \aiprovider_azureai\provider();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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']);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
];
|
||||||
|
|
||||||
|
$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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
// 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 {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
// 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 {
|
||||||
|
$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 user rate limiter.
|
||||||
|
set_config('enableuserratelimit', 1, 'aiprovider_azureai');
|
||||||
|
set_config('userratelimit', 1, 'aiprovider_azureai');
|
||||||
|
|
||||||
|
// 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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_generate_text($this->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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_generate_text($this->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.
|
||||||
|
set_config('enableglobalratelimit', 1, 'aiprovider_azureai');
|
||||||
|
set_config('globalratelimit', 1, 'aiprovider_azureai');
|
||||||
|
|
||||||
|
// 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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_generate_text($this->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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_generate_text($this->provider, $this->action);
|
||||||
|
$result = $processor->process();
|
||||||
|
$this->assertTrue($result->get_success());
|
||||||
|
}
|
||||||
|
}
|
444
ai/provider/azureai/tests/process_summarise_text_test.php
Normal file
444
ai/provider/azureai/tests/process_summarise_text_test.php
Normal file
|
@ -0,0 +1,444 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace aiprovider_azureai;
|
||||||
|
|
||||||
|
use aiprovider_azureai\process_summarise_text;
|
||||||
|
use core_ai\aiactions\base;
|
||||||
|
use core_ai\provider;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Generate text provider class for Azure AI provider methods.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @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 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();
|
||||||
|
// 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->provider = new \aiprovider_azureai\provider();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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']);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
];
|
||||||
|
|
||||||
|
$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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
// 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 {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
// 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 {
|
||||||
|
$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 user rate limiter.
|
||||||
|
set_config('enableuserratelimit', 1, 'aiprovider_azureai');
|
||||||
|
set_config('userratelimit', 1, 'aiprovider_azureai');
|
||||||
|
|
||||||
|
// 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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_summarise_text($this->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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_summarise_text($this->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.
|
||||||
|
set_config('enableglobalratelimit', 1, 'aiprovider_azureai');
|
||||||
|
set_config('globalratelimit', 1, 'aiprovider_azureai');
|
||||||
|
|
||||||
|
// 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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_summarise_text($this->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_provider();
|
||||||
|
$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($this->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_provider();
|
||||||
|
$this->create_action($user1->id);
|
||||||
|
$processor = new process_summarise_text($this->provider, $this->action);
|
||||||
|
$result = $processor->process();
|
||||||
|
$this->assertTrue($result->get_success());
|
||||||
|
}
|
||||||
|
}
|
113
ai/provider/azureai/tests/provider_test.php
Normal file
113
ai/provider/azureai/tests/provider_test.php
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace aiprovider_azureai;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Azure AI provider methods.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
* @covers \core_ai\provider\azureai
|
||||||
|
*/
|
||||||
|
final class provider_test extends \advanced_testcase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test get_action_list
|
||||||
|
*/
|
||||||
|
public function test_get_action_list(): void {
|
||||||
|
$provider = new \aiprovider_azureai\provider();
|
||||||
|
$actionlist = $provider->get_action_list();
|
||||||
|
$this->assertIsArray($actionlist);
|
||||||
|
$this->assertEquals(3, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test generate_userid.
|
||||||
|
*/
|
||||||
|
public function test_generate_userid(): void {
|
||||||
|
$provider = new \aiprovider_azureai\provider();
|
||||||
|
$userid = $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 {
|
||||||
|
$this->resetAfterTest(true);
|
||||||
|
|
||||||
|
// Set plugin config rate limiter settings.
|
||||||
|
set_config('enableglobalratelimit', 1, 'aiprovider_azureai');
|
||||||
|
set_config('globalratelimit', 5, 'aiprovider_azureai');
|
||||||
|
set_config('enableuserratelimit', 1, 'aiprovider_azureai');
|
||||||
|
set_config('userratelimit', 3, 'aiprovider_azureai');
|
||||||
|
|
||||||
|
$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
|
||||||
|
);
|
||||||
|
$provider = new provider();
|
||||||
|
|
||||||
|
// 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']);
|
||||||
|
}
|
||||||
|
}
|
30
ai/provider/azureai/version.php
Normal file
30
ai/provider/azureai/version.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version information for aiprovider_azureai.
|
||||||
|
*
|
||||||
|
* @package aiprovider_azureai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
$plugin->component = 'aiprovider_azureai';
|
||||||
|
$plugin->version = 2024061400;
|
||||||
|
$plugin->requires = 2024041600;
|
||||||
|
$plugin->maturity = MATURITY_STABLE;
|
|
@ -88,17 +88,16 @@ final class manager_test extends \advanced_testcase {
|
||||||
$this->assertEquals($actions, array_keys($providers));
|
$this->assertEquals($actions, array_keys($providers));
|
||||||
|
|
||||||
// Assert that there is only one provider for each action.
|
// Assert that there is only one provider for each action.
|
||||||
$this->assertCount(1, $providers[generate_text::class]);
|
$this->assertCount(2, $providers[generate_text::class]);
|
||||||
$this->assertCount(1, $providers[summarise_text::class]);
|
$this->assertCount(2, $providers[summarise_text::class]);
|
||||||
|
|
||||||
// Disable the generate text action for the open ai provider.
|
// Disable the generate text action for the Open AI provider.
|
||||||
set_config(generate_text::class, 0, 'aiprovider_openai');
|
set_config(generate_text::class, 0, 'aiprovider_openai');
|
||||||
$providers = $manager->get_providers_for_actions($actions, true);
|
$providers = $manager->get_providers_for_actions($actions, true);
|
||||||
|
|
||||||
// Assert that there is no provider for the generate text action.
|
// Assert that there is no provider for the generate text action.
|
||||||
$this->assertCount(0, $providers[generate_text::class]);
|
$this->assertCount(0, $providers[generate_text::class]);
|
||||||
$this->assertCount(1, $providers[summarise_text::class]);
|
$this->assertCount(1, $providers[summarise_text::class]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"editor"
|
"editor"
|
||||||
],
|
],
|
||||||
"aiprovider": [
|
"aiprovider": [
|
||||||
"openai"
|
"openai",
|
||||||
|
"azureai"
|
||||||
],
|
],
|
||||||
"antivirus": [
|
"antivirus": [
|
||||||
"clamav"
|
"clamav"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue