mirror of
https://github.com/moodle/moodle.git
synced 2025-08-09 10:56:56 +02:00
MDL-82627 AI: Provider Plugin - Open AI
Initial Open AI API Provider plugin. Originally implemented in MDL-80894 Co-authored-by: Huong Nguyen <huongnv13@gmail.com>
This commit is contained in:
parent
8186e6ce25
commit
ece707e40e
16 changed files with 1784 additions and 1 deletions
|
@ -1 +0,0 @@
|
||||||
TODO: remove this when the first plugin is added.
|
|
103
ai/provider/openai/classes/privacy/provider.php
Normal file
103
ai/provider/openai/classes/privacy/provider.php
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<?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_openai\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 provider implementation for OpenAI provider
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns meta data about this system.
|
||||||
|
*
|
||||||
|
* @param collection $collection The initialised collection to add items to.
|
||||||
|
* @return collection A listing of user data stored through this system.
|
||||||
|
*/
|
||||||
|
public static function get_metadata(collection $collection): collection {
|
||||||
|
$collection->add_external_location_link('aiprovider_openai', [
|
||||||
|
'prompttext' => 'privacy:metadata:aiprovider_openai:prompttext',
|
||||||
|
'model' => 'privacy:metadata:aiprovider_openai:model',
|
||||||
|
'numberimages' => 'privacy:metadata:aiprovider_openai:numberimages',
|
||||||
|
'responseformat' => 'privacy:metadata:aiprovider_openai:responseformat',
|
||||||
|
], 'privacy:metadata:aiprovider_openai:externalpurpose');
|
||||||
|
return $collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of contexts that contain user information for the specified user.
|
||||||
|
*
|
||||||
|
* @param int $userid The user to search.
|
||||||
|
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
|
||||||
|
*/
|
||||||
|
public static function get_contexts_for_userid(int $userid): contextlist {
|
||||||
|
return new contextlist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of users who have data within a context.
|
||||||
|
*
|
||||||
|
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||||
|
*/
|
||||||
|
public static function get_users_in_context(userlist $userlist) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all user data for the specified user, in the specified contexts.
|
||||||
|
*
|
||||||
|
* @param approved_contextlist $contextlist The approved contexts to export information for.
|
||||||
|
*/
|
||||||
|
public static function export_user_data(approved_contextlist $contextlist) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all use data which matches the specified deletion_criteria.
|
||||||
|
*
|
||||||
|
* @param \context $context A user context.
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all user data for the specified user, in the specified contexts.
|
||||||
|
*
|
||||||
|
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
|
||||||
|
*/
|
||||||
|
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||||
|
}
|
||||||
|
}
|
201
ai/provider/openai/classes/process_generate_image.php
Normal file
201
ai/provider/openai/classes/process_generate_image.php
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
<?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_openai;
|
||||||
|
|
||||||
|
use core_ai\aiactions\responses\response_base;
|
||||||
|
use core_ai\aiactions\responses\response_generate_image;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
require_once($CFG->libdir . '/filelib.php');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class process image generation.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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 process_generate_text {
|
||||||
|
/** @var string The API endpoint to make requests against. */
|
||||||
|
private string $aiendpoint = 'https://api.openai.com/v1/images/generations';
|
||||||
|
|
||||||
|
/** @var string The API model to use. */
|
||||||
|
private string $model = 'dall-e-3';
|
||||||
|
|
||||||
|
/** @var int The number of images to generate dall-e-3 only supports 1. */
|
||||||
|
private int $numberimages = 1;
|
||||||
|
|
||||||
|
/** @var string Response format: url or b64_json. */
|
||||||
|
private string $responseformat = 'url';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the AI request.
|
||||||
|
*
|
||||||
|
* @return response_base The result of the action.
|
||||||
|
*/
|
||||||
|
public function process(): response_base {
|
||||||
|
// Check the rate limiter.
|
||||||
|
$ratelimitcheck = $this->provider->is_request_allowed($this->action);
|
||||||
|
if ($ratelimitcheck !== true) {
|
||||||
|
return new response_generate_image(
|
||||||
|
success: false,
|
||||||
|
actionname: 'generate_image',
|
||||||
|
errorcode: $ratelimitcheck['errorcode'],
|
||||||
|
errormessage: $ratelimitcheck['errormessage'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userid = $this->provider->generate_userid($this->action->get_configuration('userid'));
|
||||||
|
$client = $this->provider->create_http_client($this->aiendpoint);
|
||||||
|
|
||||||
|
// Create the request object.
|
||||||
|
$requestobj = $this->create_request_object($this->action, $userid);
|
||||||
|
|
||||||
|
// Make the request to the OpenAI API.
|
||||||
|
$response = $this->query_ai_api($client, $requestobj);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the action response object.
|
||||||
|
return $this->prepare_response($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given aspect ratio to an image size
|
||||||
|
* that is compatible with the OpenAI 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the request object to send to the OpenAI API.
|
||||||
|
* This object contains all the required parameters for the request.
|
||||||
|
*
|
||||||
|
* @param \core_ai\aiactions\base $action The action to process.
|
||||||
|
* @param string $userid The user id.
|
||||||
|
* @return \stdClass The request object to send to the OpenAI API.
|
||||||
|
* @throws \coding_exception
|
||||||
|
*/
|
||||||
|
private function create_request_object(\core_ai\aiactions\base $action, string $userid): \stdClass {
|
||||||
|
$requestobj = new \stdClass();
|
||||||
|
$requestobj->prompt = $action->get_configuration('prompttext');
|
||||||
|
$requestobj->model = $this->model;
|
||||||
|
$requestobj->n = $this->numberimages;
|
||||||
|
$requestobj->quality = $action->get_configuration('quality');
|
||||||
|
$requestobj->response_format = $this->responseformat;
|
||||||
|
$requestobj->size = $this->calculate_size($action->get_configuration('aspectratio'));
|
||||||
|
$requestobj->style = $action->get_configuration('style');
|
||||||
|
$requestobj->user = $userid;
|
||||||
|
|
||||||
|
return $requestobj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a successful response from the external AI api.
|
||||||
|
*
|
||||||
|
* @param ResponseInterface $response The response object.
|
||||||
|
* @return array The response.
|
||||||
|
*/
|
||||||
|
protected function handle_api_success(ResponseInterface $response): array {
|
||||||
|
$responsebody = $response->getBody();
|
||||||
|
$bodyobj = json_decode($responsebody->getContents());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'sourceurl' => $bodyobj->data[0]->url,
|
||||||
|
'revisedprompt' => $bodyobj->data[0]->revised_prompt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the response object.
|
||||||
|
*
|
||||||
|
* @param array $response The response object.
|
||||||
|
* @return response_generate_image The action response object.
|
||||||
|
* @throws \coding_exception
|
||||||
|
*/
|
||||||
|
private function prepare_response(array $response): response_generate_image {
|
||||||
|
if ($response['success']) {
|
||||||
|
$generatedimage = new response_generate_image(
|
||||||
|
success: true,
|
||||||
|
actionname: 'generate_image',
|
||||||
|
);
|
||||||
|
$generatedimage->set_response($response);
|
||||||
|
return $generatedimage;
|
||||||
|
} else {
|
||||||
|
return new response_generate_image(
|
||||||
|
success: false,
|
||||||
|
actionname: 'generate_image',
|
||||||
|
errorcode: $response['errorcode'],
|
||||||
|
errormessage: $response['errormessage'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
$parsedurl = parse_url($url, PHP_URL_PATH); // Parse the URL to get the path.
|
||||||
|
$filename = basename($parsedurl); // Get the basename of the path.
|
||||||
|
|
||||||
|
// 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_url($fileinfo, $url);
|
||||||
|
}
|
||||||
|
}
|
209
ai/provider/openai/classes/process_generate_text.php
Normal file
209
ai/provider/openai/classes/process_generate_text.php
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
<?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_openai;
|
||||||
|
|
||||||
|
use core\http_client;
|
||||||
|
use core_ai\aiactions\responses\response_base;
|
||||||
|
use core_ai\aiactions\responses\response_generate_text;
|
||||||
|
use core_ai\process_base;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class process text generation.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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 process_base {
|
||||||
|
/** @var string The API endpoint to make requests against. */
|
||||||
|
private string $aiendpoint = 'https://api.openai.com/v1/chat/completions';
|
||||||
|
|
||||||
|
/** @var string The API model to use. */
|
||||||
|
private string $model = 'gpt-4o';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the AI request.
|
||||||
|
*
|
||||||
|
* @return response_base The result of the action.
|
||||||
|
*/
|
||||||
|
public function process(): response_base {
|
||||||
|
// Check the rate limiter.
|
||||||
|
$ratelimitcheck = $this->provider->is_request_allowed($this->action);
|
||||||
|
if ($ratelimitcheck !== true) {
|
||||||
|
return new response_generate_text(
|
||||||
|
success: false,
|
||||||
|
actionname: 'generate_text',
|
||||||
|
errorcode: $ratelimitcheck['errorcode'],
|
||||||
|
errormessage: $ratelimitcheck['errormessage'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userid = $this->provider->generate_userid($this->action->get_configuration('userid'));
|
||||||
|
$client = $this->provider->create_http_client($this->aiendpoint);
|
||||||
|
|
||||||
|
// Create the request object.
|
||||||
|
$requestobj = $this->create_request_object($this->action, $userid);
|
||||||
|
|
||||||
|
// Make the request to the OpenAI API.
|
||||||
|
$response = $this->query_ai_api($client, $requestobj);
|
||||||
|
|
||||||
|
// Format the action response object.
|
||||||
|
return $this->prepare_response($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the AI service.
|
||||||
|
*
|
||||||
|
* @param http_client $client The http client.
|
||||||
|
* @param \stdClass $requestobj The request object.
|
||||||
|
* @return array The response from the AI service.
|
||||||
|
*/
|
||||||
|
protected function query_ai_api(http_client $client, \stdClass $requestobj): array {
|
||||||
|
$requestjson = json_encode($requestobj);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the external AI service.
|
||||||
|
$response = $client->request('POST', '', [
|
||||||
|
'body' => $requestjson,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 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($status, $response);
|
||||||
|
}
|
||||||
|
} catch (RequestException $e) {
|
||||||
|
// Handle any exceptions.
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'errorcode' => $e->getCode(),
|
||||||
|
'errormessage' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the request object to send to the OpenAI API.
|
||||||
|
* This object contains all the required parameters for the request.
|
||||||
|
*
|
||||||
|
* @param \core_ai\aiactions\base $action The action to process.
|
||||||
|
* @param string $userid The user id.
|
||||||
|
* @return \stdClass The request object to send to the OpenAI API.
|
||||||
|
* @throws \coding_exception
|
||||||
|
*/
|
||||||
|
private function create_request_object(\core_ai\aiactions\base $action, string $userid): \stdClass {
|
||||||
|
// Create the user object.
|
||||||
|
$userobj = new \stdClass();
|
||||||
|
$userobj->role = 'user';
|
||||||
|
$userobj->content = $action->get_configuration('prompttext');
|
||||||
|
|
||||||
|
// Create the request object.
|
||||||
|
$requestobj = new \stdClass();
|
||||||
|
$requestobj->model = $this->model;
|
||||||
|
$requestobj->user = $userid;
|
||||||
|
|
||||||
|
// If there is a system string available, use it.
|
||||||
|
$systeminstruction = $action->get_system_instruction();
|
||||||
|
if (!empty($systeminstruction)) {
|
||||||
|
$systemobj = new \stdClass();
|
||||||
|
$systemobj->role = 'system';
|
||||||
|
$systemobj->content = $systeminstruction;
|
||||||
|
$requestobj->messages = [$systemobj, $userobj];
|
||||||
|
} else {
|
||||||
|
$requestobj->messages = [$userobj];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $requestobj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an error from the external AI api.
|
||||||
|
*
|
||||||
|
* @param int $status The status code.
|
||||||
|
* @param ResponseInterface $response The response object.
|
||||||
|
* @return array The error response.
|
||||||
|
*/
|
||||||
|
protected function handle_api_error(int $status, ResponseInterface $response): array {
|
||||||
|
$responsearr = [
|
||||||
|
'success' => false,
|
||||||
|
'errorcode' => $status,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($status == 500) {
|
||||||
|
$responsearr['errormessage'] = 'Internal server error.';
|
||||||
|
} else if ($status == 503) {
|
||||||
|
$responsearr['errormessage'] = 'Service unavailable.';
|
||||||
|
} else {
|
||||||
|
$responsebody = $response->getBody();
|
||||||
|
$bodyobj = json_decode($responsebody->getContents());
|
||||||
|
$responsearr['errormessage'] = $bodyobj->error->message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $responsearr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the response object.
|
||||||
|
*
|
||||||
|
* @param array $response The response object.
|
||||||
|
* @return response_generate_text The action response object.
|
||||||
|
* @throws \coding_exception
|
||||||
|
*/
|
||||||
|
private function prepare_response(array $response): response_generate_text {
|
||||||
|
if ($response['success']) {
|
||||||
|
$generatedtext = new response_generate_text(
|
||||||
|
success: true,
|
||||||
|
actionname: 'generate_text',
|
||||||
|
);
|
||||||
|
$generatedtext->set_response($response);
|
||||||
|
return $generatedtext;
|
||||||
|
} else {
|
||||||
|
return new response_generate_text(
|
||||||
|
success: false,
|
||||||
|
actionname: 'generate_text',
|
||||||
|
errorcode: $response['errorcode'],
|
||||||
|
errormessage: $response['errormessage'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
ai/provider/openai/classes/process_summarise_text.php
Normal file
53
ai/provider/openai/classes/process_summarise_text.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?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_openai;
|
||||||
|
|
||||||
|
use core_ai\aiactions\responses\response_summarise_text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class process text summarisation.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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 {
|
||||||
|
/**
|
||||||
|
* Prepare the response object.
|
||||||
|
*
|
||||||
|
* @param array $response The response object.
|
||||||
|
* @return response_summarise_text The action response object.
|
||||||
|
* @throws \coding_exception
|
||||||
|
*/
|
||||||
|
private function prepare_response(array $response): response_summarise_text {
|
||||||
|
if ($response['success']) {
|
||||||
|
$generatedtext = new response_summarise_text(
|
||||||
|
success: true,
|
||||||
|
actionname: 'summarise_text',
|
||||||
|
);
|
||||||
|
$generatedtext->set_response($response);
|
||||||
|
return $generatedtext;
|
||||||
|
} else {
|
||||||
|
return new response_summarise_text(
|
||||||
|
success: false,
|
||||||
|
actionname: 'summarise_text',
|
||||||
|
errorcode: $response['errorcode'],
|
||||||
|
errormessage: $response['errormessage'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
ai/provider/openai/classes/provider.php
Normal file
149
ai/provider/openai/classes/provider.php
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
<?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_openai;
|
||||||
|
|
||||||
|
use core\http_client;
|
||||||
|
use core_ai\aiactions;
|
||||||
|
use core_ai\ratelimiter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class provider.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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 openAI API key. */
|
||||||
|
private string $apikey;
|
||||||
|
|
||||||
|
/** @var string The organisation ID that goes with the key. */
|
||||||
|
private string $orgid;
|
||||||
|
|
||||||
|
/** @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_openai', 'apikey');
|
||||||
|
// Get api org id from config.
|
||||||
|
$this->orgid = get_config('aiprovider_openai', 'orgid');
|
||||||
|
// Get global rate limit from config.
|
||||||
|
$this->enableglobalratelimit = get_config('aiprovider_openai', 'enableglobalratelimit');
|
||||||
|
$this->globalratelimit = get_config('aiprovider_openai', 'globalratelimit');
|
||||||
|
// Get user rate limit from config.
|
||||||
|
$this->enableuserratelimit = get_config('aiprovider_openai', 'enableuserratelimit');
|
||||||
|
$this->userratelimit = get_config('aiprovider_openai', '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 OpenAI.
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HTTP client.
|
||||||
|
*
|
||||||
|
* @param string $apiendpoint The API endpoint.
|
||||||
|
* @return http_client The HTTP client used to make requests.
|
||||||
|
*/
|
||||||
|
public function create_http_client(string $apiendpoint): http_client {
|
||||||
|
return new http_client([
|
||||||
|
'base_uri' => $apiendpoint,
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer ' . $this->apikey,
|
||||||
|
'OpenAI-Organization' => $this->orgid,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = ratelimiter::get_instance();
|
||||||
|
$component = explode('\\', get_class($this))[0];
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
43
ai/provider/openai/lang/en/aiprovider_openai.php
Normal file
43
ai/provider/openai/lang/en/aiprovider_openai.php
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?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_openai, language 'en'.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
$string['apikey'] = 'OpenAI API key';
|
||||||
|
$string['apikey_desc'] = 'Enter your OpenAI API key. You can get one from https://platform.openai.com/account/api-keys';
|
||||||
|
$string['enableglobalratelimit'] = 'Enable global rate limiting';
|
||||||
|
$string['enableglobalratelimit_desc'] = 'Enable global rate limiting for the OpenAI API provider.';
|
||||||
|
$string['enableuserratelimit'] = 'Enable user rate limiting';
|
||||||
|
$string['enableuserratelimit_desc'] = 'Enable user rate limiting for the OpenAI API provider.';
|
||||||
|
$string['globalratelimit'] = 'Global rate limit';
|
||||||
|
$string['globalratelimit_desc'] = 'Set the number of requests per hour allowed for the global rate limit.';
|
||||||
|
$string['orgid'] = 'OpenAI organization ID';
|
||||||
|
$string['orgid_desc'] = 'Enter your OpenAI organization ID. You can get one from https://platform.openai.com/account/org-settings';
|
||||||
|
$string['pluginname'] = 'OpenAI API Provider';
|
||||||
|
$string['privacy:metadata'] = 'The OpenAI API provider plugin does not store any personal data.';
|
||||||
|
$string['privacy:metadata:aiprovider_openai:externalpurpose'] = 'This information is sent to the OpenAI API in order for a response to be generated. Your OpenAI account settings may change how OpenAI stores and retains this data. No user data is explicitly sent to OpenAI or stored in Moodle LMS by this plugin.';
|
||||||
|
$string['privacy:metadata:aiprovider_openai:model'] = 'The model used to generate the response.';
|
||||||
|
$string['privacy:metadata:aiprovider_openai:numberimages'] = 'The number of images used in the response. When generating images.';
|
||||||
|
$string['privacy:metadata:aiprovider_openai:prompttext'] = 'The user entered text prompt used to generate the response.';
|
||||||
|
$string['privacy:metadata:aiprovider_openai: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.';
|
80
ai/provider/openai/settings.php
Normal file
80
ai/provider/openai/settings.php
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<?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_openai
|
||||||
|
* @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 heading.
|
||||||
|
$settings = new admin_settingspage_provider('aiprovider_openai',
|
||||||
|
new lang_string('pluginname', 'aiprovider_openai'), 'moodle/site:config', true);
|
||||||
|
|
||||||
|
$settings->add(new admin_setting_heading('aiprovider_openai/general',
|
||||||
|
new lang_string('providersettings', 'core_ai'),
|
||||||
|
new lang_string('providersettings_desc', 'core_ai')));
|
||||||
|
|
||||||
|
// Setting to store OpenAI API key.
|
||||||
|
$settings->add(new admin_setting_configpasswordunmask('aiprovider_openai/apikey',
|
||||||
|
new lang_string('apikey', 'aiprovider_openai'),
|
||||||
|
new lang_string('apikey_desc', 'aiprovider_openai'),
|
||||||
|
''));
|
||||||
|
|
||||||
|
// Setting to store OpenAI organization ID.
|
||||||
|
$settings->add(new admin_setting_configtext('aiprovider_openai/orgid',
|
||||||
|
new lang_string('orgid', 'aiprovider_openai'),
|
||||||
|
new lang_string('orgid_desc', 'aiprovider_openai'),
|
||||||
|
'',
|
||||||
|
PARAM_TEXT));
|
||||||
|
|
||||||
|
// Setting to enable/disable global rate limiting.
|
||||||
|
$settings->add(new admin_setting_configcheckbox('aiprovider_openai/enableglobalratelimit',
|
||||||
|
new lang_string('enableglobalratelimit', 'aiprovider_openai'),
|
||||||
|
new lang_string('enableglobalratelimit_desc', 'aiprovider_openai'),
|
||||||
|
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_openai/globalratelimit',
|
||||||
|
new lang_string('globalratelimit', 'aiprovider_openai'),
|
||||||
|
new lang_string('globalratelimit_desc', 'aiprovider_openai'),
|
||||||
|
100,
|
||||||
|
PARAM_INT));
|
||||||
|
$settings->hide_if('aiprovider_openai/globalratelimit', 'aiprovider_openai/enableglobalratelimit', 'eq', 0);
|
||||||
|
|
||||||
|
// Setting to enable/disable user rate limiting.
|
||||||
|
$settings->add(new admin_setting_configcheckbox('aiprovider_openai/enableuserratelimit',
|
||||||
|
new lang_string('enableuserratelimit', 'aiprovider_openai'),
|
||||||
|
new lang_string('enableuserratelimit_desc', 'aiprovider_openai'),
|
||||||
|
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_openai/userratelimit',
|
||||||
|
new lang_string('userratelimit', 'aiprovider_openai'),
|
||||||
|
new lang_string('userratelimit_desc', 'aiprovider_openai'),
|
||||||
|
10,
|
||||||
|
PARAM_INT));
|
||||||
|
$settings->hide_if('aiprovider_openai/userratelimit', 'aiprovider_openai/enableuserratelimit', 'eq', 0);
|
||||||
|
}
|
9
ai/provider/openai/tests/fixtures/image_request_success.json
vendored
Normal file
9
ai/provider/openai/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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
23
ai/provider/openai/tests/fixtures/text_request_success.json
vendored
Normal file
23
ai/provider/openai/tests/fixtures/text_request_success.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"id":"chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo",
|
||||||
|
"object":"chat.completion",
|
||||||
|
"created":1721168885,
|
||||||
|
"model":"gpt-4o-2024-05-13",
|
||||||
|
"choices":[
|
||||||
|
{
|
||||||
|
"index":0,
|
||||||
|
"message":{
|
||||||
|
"role":"assistant",
|
||||||
|
"content":"Sure, here is some sample text in various styles to suit different testing needs:\n\n### Formal Business Email\nSubject: Proposal for Quarterly Marketing Strategy Meeting\n\nDear Ms. Johnson,\n\nI hope this message finds you well.\n\nI am writing to propose a meeting to discuss our marketing strategy for the upcoming quarter. Given our recent performance metrics, I believe it is essential to realign our objectives and explore new opportunities for growth.\n\nCould we schedule a meeting next Tuesday at 2 PM? Please let me know your availability or suggest an alternative time that suits you.\n\nThank you for your attention to this matter. I look forward to your response.\n\nBest regards,\n\nJohn Smith \nMarketing Manager \nABC Corporation \njohn.smith@abccorp.com \n(123) 456-7890\n\n---\n\n### Casual Text Message\nHey Emma,\n\nJust a heads up, a few of us are planning to get together this Friday evening at Joe's Place. Would love for you to join us! Let me know if you're free.\n\nCheers, \nAlex\n\n---\n\n### Technical Documentation\n**Function: calculateAverage**\n\n**Description:** \nThis function takes an array of numerical values as input and returns the average of those values.\n\n**Parameters:** \n- `numbers` (Array of Float): The input array containing numerical values.\n\n**Returns:** \n- `Float`: The average of the input values.\n\n**Example:**\n```python\ndef calculateAverage(numbers):\n if not numbers:\n return 0\n return sum(numbers) / len(numbers)\n\n# Example usage\ndata = [10, 20, 30, 40, 50]\naverage = calculateAverage(data)\nprint(f\"The average is: {average}\")\n```\n\n---\n\n### Narrative Paragraph\nThe sun dipped below the horizon, casting a warm golden glow over the rolling hills. Sarah stood still, taking in the breathtaking view, her heart filled with a mix of serenity and longing. As the first stars began to twinkle in the twilight sky, she knew that this moment, fleeting and beautiful, would stay with her forever—the end of one journey and the quiet beginning of another.\n\n---\n\n### Product Description\n**Portable Bluetooth Speaker**\n\nExperience top-quality sound on the go with our Portable Bluetooth Speaker. Lightweight and compact, this device offers crystal clear audio and robust bass. Its long-lasting battery ensures up to 12 hours of uninterrupted music, making it perfect for outdoor adventures, parties, and travel. With easy Bluetooth connectivity, you can pair it with any device in seconds and enjoy your favorite tunes anywhere, anytime.\n\n**Features:**\n- Up to 12 hours of playtime\n- Superior sound quality with deep bass\n- Lightweight and portable design\n- Quick and easy Bluetooth pairing\n- Durable, sleek exterior\n\n---\n\nFeel free to adjust the text according to your specific testing needs!"
|
||||||
|
},
|
||||||
|
"logprobs":null,
|
||||||
|
"finish_reason":"stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage":{
|
||||||
|
"prompt_tokens":11,
|
||||||
|
"completion_tokens":568,
|
||||||
|
"total_tokens":579
|
||||||
|
},
|
||||||
|
"system_fingerprint":"fp_c4e5b6fa31"
|
||||||
|
}
|
316
ai/provider/openai/tests/process_generate_image_test.php
Normal file
316
ai/provider/openai/tests/process_generate_image_test.php
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
<?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_openai;
|
||||||
|
|
||||||
|
use core_ai\aiactions\base;
|
||||||
|
use core_ai\provider;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test response_base OpenAI provider methods.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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\openai
|
||||||
|
*/
|
||||||
|
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(__DIR__ . '/fixtures/image_request_success.json');
|
||||||
|
$this->provider = new \aiprovider_openai\provider();
|
||||||
|
$this->action = new \core_ai\aiactions\generate_image(
|
||||||
|
contextid: 1,
|
||||||
|
userid: 1,
|
||||||
|
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, $this->action, 1);
|
||||||
|
|
||||||
|
$this->assertEquals('This is a test prompt', $request->prompt);
|
||||||
|
$this->assertEquals('dall-e-3', $request->model);
|
||||||
|
$this->assertEquals('1', $request->n);
|
||||||
|
$this->assertEquals('hd', $request->quality);
|
||||||
|
$this->assertEquals('url', $request->response_format);
|
||||||
|
$this->assertEquals('1024x1024', $request->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, $status, $response);
|
||||||
|
$this->assertEquals($status, $result['errorcode']);
|
||||||
|
if ($status == 500) {
|
||||||
|
$this->assertEquals('Internal server error.', $result['errormessage']);
|
||||||
|
} else if ($status == 503) {
|
||||||
|
$this->assertEquals('Service unavailable.', $result['errormessage']);
|
||||||
|
} else {
|
||||||
|
$this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the API success response handler method.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function test_handle_api_success(): void {
|
||||||
|
$response = new Response(
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/json'],
|
||||||
|
$this->responsebodyjson
|
||||||
|
);
|
||||||
|
|
||||||
|
// We're testing a private method, so we need to setup reflector magic.
|
||||||
|
$processor = new process_generate_image($this->provider, $this->action);
|
||||||
|
$method = new \ReflectionMethod($processor, 'handle_api_success');
|
||||||
|
|
||||||
|
$result = $method->invoke($processor, $response);
|
||||||
|
|
||||||
|
$this->stringContains('An image that represents the concept of a \'test\'.', $result['revisedprompt']);
|
||||||
|
$this->stringContains('oaidalleapiprodscus.blob.core.windows.net', $result['sourceurl']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test query_ai_api for a successful call.
|
||||||
|
*/
|
||||||
|
public function test_query_ai_api_success(): void {
|
||||||
|
// Mock the http client to return a successful response.
|
||||||
|
$response = new Response(
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/json'],
|
||||||
|
$this->responsebodyjson
|
||||||
|
);
|
||||||
|
$client = $this->createMock(\core\http_client::class);
|
||||||
|
$client->method('request')->willReturn($response);
|
||||||
|
|
||||||
|
// 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, $client, $requestobj);
|
||||||
|
|
||||||
|
$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()['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);
|
||||||
|
$filenobj = $method->invoke($processor, $contextid, $url);
|
||||||
|
|
||||||
|
$this->assertEquals('test.jpg', $filenobj->get_filename());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
$url = $this->getExternalTestFileUrl('/test.jpg', false);
|
||||||
|
|
||||||
|
$responsebodyjson = json_encode([
|
||||||
|
'created' => 1719140500,
|
||||||
|
'data' => [
|
||||||
|
(object) [
|
||||||
|
'revised_prompt' => 'An image that represents the concept of a \'test\'.',
|
||||||
|
'url' => $url,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = new Response(
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/json'],
|
||||||
|
$responsebodyjson,
|
||||||
|
);
|
||||||
|
|
||||||
|
$mockhttpclient = $this->createMock(\core\http_client::class);
|
||||||
|
$mockhttpclient->method('request')->willReturn($response);
|
||||||
|
|
||||||
|
// Mock the provider to return the mocked http client.
|
||||||
|
$mockprovider = $this->getMockBuilder(\aiprovider_openai\provider::class)
|
||||||
|
->onlyMethods(['create_http_client'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$mockprovider->method('create_http_client')->willReturn($mockhttpclient);
|
||||||
|
|
||||||
|
// 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($mockprovider, $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()['revisedprompt']);
|
||||||
|
$this->assertEquals($url, $result->get_response()['sourceurl']);
|
||||||
|
}
|
||||||
|
}
|
218
ai/provider/openai/tests/process_generate_text_test.php
Normal file
218
ai/provider/openai/tests/process_generate_text_test.php
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
<?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_openai;
|
||||||
|
|
||||||
|
use aiprovider_openai\process_generate_text;
|
||||||
|
use core_ai\aiactions\base;
|
||||||
|
use core_ai\provider;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Generate text provider class for OpenAI provider methods.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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\openai
|
||||||
|
*/
|
||||||
|
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(__DIR__ . '/fixtures/text_request_success.json');
|
||||||
|
$this->provider = new \aiprovider_openai\provider();
|
||||||
|
$this->action = new \core_ai\aiactions\generate_text(
|
||||||
|
contextid: 1,
|
||||||
|
userid: 1,
|
||||||
|
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, $this->action, 1);
|
||||||
|
|
||||||
|
$this->assertEquals('This is a test prompt', $request->messages[0]->content);
|
||||||
|
$this->assertEquals('user', $request->messages[0]->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_generate_text($this->provider, $this->action);
|
||||||
|
$method = new \ReflectionMethod($processor, 'handle_api_error');
|
||||||
|
|
||||||
|
foreach ($responses as $status => $response) {
|
||||||
|
$result = $method->invoke($processor, $status, $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-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']);
|
||||||
|
$this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']);
|
||||||
|
$this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']);
|
||||||
|
$this->assertEquals('stop', $result['finishreason']);
|
||||||
|
$this->assertEquals('11', $result['prompttokens']);
|
||||||
|
$this->assertEquals('568', $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.
|
||||||
|
$response = new Response(
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/json'],
|
||||||
|
$this->responsebodyjson
|
||||||
|
);
|
||||||
|
$client = $this->createMock(\core\http_client::class);
|
||||||
|
$client->method('request')->willReturn($response);
|
||||||
|
|
||||||
|
// Create a request object.
|
||||||
|
$requestobj = new \stdClass();
|
||||||
|
$requestobj->model = 'gpt-4o';
|
||||||
|
$requestobj->user = 't3464h89dftjltestudfaser';
|
||||||
|
|
||||||
|
$userobj = new \stdClass();
|
||||||
|
$userobj->role = 'user';
|
||||||
|
$userobj->content = 'This is a test prompt';
|
||||||
|
|
||||||
|
$requestobj->messages = [$userobj];
|
||||||
|
|
||||||
|
$processor = new process_generate_text($this->provider, $this->action);
|
||||||
|
$method = new \ReflectionMethod($processor, 'query_ai_api');
|
||||||
|
$result = $method->invoke($processor, $client, $requestobj);
|
||||||
|
|
||||||
|
$this->assertTrue($result['success']);
|
||||||
|
$this->assertEquals('chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']);
|
||||||
|
$this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']);
|
||||||
|
$this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']);
|
||||||
|
$this->assertEquals('stop', $result['finishreason']);
|
||||||
|
$this->assertEquals('11', $result['prompttokens']);
|
||||||
|
$this->assertEquals('568', $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' => '568',
|
||||||
|
];
|
||||||
|
|
||||||
|
$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()['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());
|
||||||
|
}
|
||||||
|
}
|
218
ai/provider/openai/tests/process_summarise_text_test.php
Normal file
218
ai/provider/openai/tests/process_summarise_text_test.php
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
<?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_openai;
|
||||||
|
|
||||||
|
use aiprovider_openai\process_summarise_text;
|
||||||
|
use core_ai\aiactions\base;
|
||||||
|
use core_ai\provider;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Generate text provider class for OpenAI provider methods.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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\openai
|
||||||
|
*/
|
||||||
|
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(__DIR__ . '/fixtures/text_request_success.json');
|
||||||
|
$this->provider = new \aiprovider_openai\provider();
|
||||||
|
$this->action = new \core_ai\aiactions\summarise_text(
|
||||||
|
contextid: 1,
|
||||||
|
userid: 1,
|
||||||
|
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, $this->action, 1);
|
||||||
|
|
||||||
|
$this->assertEquals('This is a test prompt', $request->messages[1]->content);
|
||||||
|
$this->assertEquals('user', $request->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, $status, $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-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']);
|
||||||
|
$this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']);
|
||||||
|
$this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']);
|
||||||
|
$this->assertEquals('stop', $result['finishreason']);
|
||||||
|
$this->assertEquals('11', $result['prompttokens']);
|
||||||
|
$this->assertEquals('568', $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.
|
||||||
|
$response = new Response(
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/json'],
|
||||||
|
$this->responsebodyjson
|
||||||
|
);
|
||||||
|
$client = $this->createMock(\core\http_client::class);
|
||||||
|
$client->method('request')->willReturn($response);
|
||||||
|
|
||||||
|
// Create a request object.
|
||||||
|
$requestobj = new \stdClass();
|
||||||
|
$requestobj->model = 'gpt-4o';
|
||||||
|
$requestobj->user = 't3464h89dftjltestudfaser';
|
||||||
|
|
||||||
|
$userobj = new \stdClass();
|
||||||
|
$userobj->role = 'user';
|
||||||
|
$userobj->content = 'This is a test prompt';
|
||||||
|
|
||||||
|
$requestobj->messages = [$userobj];
|
||||||
|
|
||||||
|
$processor = new process_summarise_text($this->provider, $this->action);
|
||||||
|
$method = new \ReflectionMethod($processor, 'query_ai_api');
|
||||||
|
$result = $method->invoke($processor, $client, $requestobj);
|
||||||
|
|
||||||
|
$this->assertTrue($result['success']);
|
||||||
|
$this->assertEquals('chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']);
|
||||||
|
$this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']);
|
||||||
|
$this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']);
|
||||||
|
$this->assertEquals('stop', $result['finishreason']);
|
||||||
|
$this->assertEquals('11', $result['prompttokens']);
|
||||||
|
$this->assertEquals('568', $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()['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());
|
||||||
|
}
|
||||||
|
}
|
129
ai/provider/openai/tests/provider_test.php
Normal file
129
ai/provider/openai/tests/provider_test.php
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<?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_openai;
|
||||||
|
|
||||||
|
use core_ai\ratelimiter;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test OpenAI provider methods.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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\openai
|
||||||
|
*/
|
||||||
|
final class provider_test extends \advanced_testcase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test get_action_list
|
||||||
|
*/
|
||||||
|
public function test_get_action_list(): void {
|
||||||
|
$provider = new \aiprovider_openai\provider();
|
||||||
|
$actionlist = $provider->get_action_list();
|
||||||
|
$this->assertIsArray($actionlist);
|
||||||
|
$this->assertCount(3, $actionlist);
|
||||||
|
$this->assertContains('core_ai\\aiactions\\generate_text', $actionlist);
|
||||||
|
$this->assertContains('core_ai\\aiactions\\generate_image', $actionlist);
|
||||||
|
$this->assertContains('core_ai\\aiactions\\summarise_text', $actionlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test generate_userid.
|
||||||
|
*/
|
||||||
|
public function test_generate_userid(): void {
|
||||||
|
$provider = new \aiprovider_openai\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 create_http_client.
|
||||||
|
*/
|
||||||
|
public function test_create_http_client(): void {
|
||||||
|
$provider = new \aiprovider_openai\provider();
|
||||||
|
$url = 'https://api.openai.com/v1/images/generations';
|
||||||
|
$client = $provider->create_http_client($url);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(\core\http_client::class, $client);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test is_request_allowed.
|
||||||
|
*/
|
||||||
|
public function test_is_request_allowed(): void {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
ratelimiter::reset_instance(); // Reset the singleton instance.
|
||||||
|
|
||||||
|
// Set plugin config rate limiter settings.
|
||||||
|
set_config('enableglobalratelimit', 1, 'aiprovider_openai');
|
||||||
|
set_config('globalratelimit', 5, 'aiprovider_openai');
|
||||||
|
set_config('enableuserratelimit', 1, 'aiprovider_openai');
|
||||||
|
set_config('userratelimit', 3, 'aiprovider_openai');
|
||||||
|
|
||||||
|
$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 \aiprovider_openai\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/openai/version.php
Normal file
30
ai/provider/openai/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_openai.
|
||||||
|
*
|
||||||
|
* @package aiprovider_openai
|
||||||
|
* @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_openai';
|
||||||
|
$plugin->version = 2024060900;
|
||||||
|
$plugin->requires = 2024041600;
|
||||||
|
$plugin->maturity = MATURITY_ALPHA;
|
|
@ -1,5 +1,8 @@
|
||||||
{
|
{
|
||||||
"standard": {
|
"standard": {
|
||||||
|
"aiprovider": [
|
||||||
|
"openai"
|
||||||
|
],
|
||||||
"antivirus": [
|
"antivirus": [
|
||||||
"clamav"
|
"clamav"
|
||||||
],
|
],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue