Merge branch 'MDL-82980-main' of https://github.com/HuongNV13/moodle

This commit is contained in:
Shamim Rezaie 2025-02-20 12:15:33 +11:00
commit 809849ef34
31 changed files with 1126 additions and 148 deletions

View file

@ -0,0 +1,15 @@
issueNumber: MDL-82980
notes:
core_ai:
- message: |2
- The `\core_ai\form\action_settings_form` class has been updated to automatically include action buttons such as Save and Cancel.
- AI provider plugins should update their form classes by removing the `$this->add_action_buttons();` call, as it is no longer required.
type: changed
- message: >
- A new hook, `\core_ai\hook\after_ai_action_settings_form_hook`, has
been introduced. It will allows AI provider plugins to add additional
form elements for action settings configuration.
type: improved
- message: |2
- AI provider plugins that want to implement `pre-defined models` and display additional settings for models must now extend the `\core_ai\aimodel\base` class.
type: improved

View file

@ -0,0 +1,61 @@
<?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 core_ai\aimodel;
use MoodleQuickForm;
/**
* Base Model class.
*
* @package core_ai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base {
/**
* Get the display name of the model.
* This name is used to display the model in the UI.
*
* @return string The display name of the model.
*/
abstract public function get_model_display_name(): string;
/**
* Get the name of the model.
* This name is used to identify the model. The system will use this model name to make the request to the AI services.
*
* @return string The name of the model.
*/
abstract public function get_model_name(): string;
/**
* Add the model settings to the form.
*
* @param MoodleQuickForm $mform The form to add the model settings to.
*/
public function add_model_settings(MoodleQuickForm $mform): void {
}
/**
* Check if the model has settings.
*
* @return bool Whether the model has settings.
*/
public function has_model_settings(): bool {
return false;
}
}

View file

@ -34,6 +34,24 @@ class action_settings_form extends moodleform {
protected function definition() {
}
#[\Override]
protected function after_definition() {
parent::after_definition();
$this->_form->_registerCancelButton('cancel');
}
#[\Override]
public function definition_after_data() {
// Dispatch a hook for plugins to add their fields.
$hook = new \core_ai\hook\after_ai_action_settings_form_hook(
mform: $this->_form,
plugin: $this->_customdata['providername'],
);
\core\di::get(\core\hook\manager::class)->dispatch($hook);
// Add action buttons.
$this->add_action_buttons();
}
/**
* Get the default values for the form.
*

View file

@ -0,0 +1,48 @@
<?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 core_ai\hook;
use MoodleQuickForm;
/**
* Hook after AI action settings form is initiated.
*
* @package core_ai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @property-read MoodleQuickForm $mform The form element
* @property-read ?string $plugin Name of the model
*/
#[\core\attribute\label('Allows plugins to add form elements for action settings setup.')]
#[\core\attribute\tags('ai')]
class after_ai_action_settings_form_hook {
/**
* Constructor for the hook.
*
* @param MoodleQuickForm $mform The moodle form instance.
* @param ?string $plugin The name of the plugin
*/
public function __construct(
/** @var MoodleQuickForm The moodle form instance. */
public readonly MoodleQuickForm $mform,
/** @var ?string The name of the plugin */
public readonly ?string $plugin,
) {
}
}

View file

@ -366,6 +366,7 @@ class manager {
* @param string $name The name of the provider config.
* @param bool $enabled The enabled state of the provider.
* @param array|null $config The config json.
* @param array|null $actionconfig The action config json.
* @return provider
*/
public function create_provider_instance(
@ -373,6 +374,7 @@ class manager {
string $name,
bool $enabled = false,
?array $config = null,
?array $actionconfig = null,
): provider {
if (!class_exists($classname) || !is_a($classname, provider::class, true)) {
throw new \coding_exception("Provider class not valid: {$classname}");
@ -381,6 +383,7 @@ class manager {
enabled: $enabled,
name: $name,
config: json_encode($config ?? []),
actionconfig: $actionconfig ? json_encode($actionconfig) : '',
);
$id = $this->db->insert_record('ai_providers', $provider->to_record());

View file

@ -32,7 +32,7 @@ $provider = required_param('provider', PARAM_PLUGIN);
$action = required_param('action', PARAM_TEXT);
$id = required_param('providerid', PARAM_INT);
$returnurl = optional_param('returnurl', null, PARAM_LOCALURL);
$data = ['providerid' => $id];
$customdata = ['providerid' => $id];
// Handle return URL.
if (empty($returnurl)) {
@ -43,7 +43,7 @@ if (empty($returnurl)) {
} else {
$returnurl = new moodle_url($returnurl);
}
$data['returnurl'] = $returnurl;
$customdata['returnurl'] = $returnurl;
$manager = \core\di::get(\core_ai\manager::class);
$providerrecord = $manager->get_provider_records(['id' => $id]);
@ -52,7 +52,8 @@ $providerrecord = reset($providerrecord);
$actionconfig = json_decode($providerrecord->actionconfig, true, 512, JSON_THROW_ON_ERROR);
$actionconfig = $actionconfig[$action];
$data['actionconfig'] = $actionconfig;
$customdata['actionconfig'] = $actionconfig;
$customdata['providername'] = $provider;
$urlparams = [
'provider' => $provider,
@ -69,7 +70,7 @@ $PAGE->set_title($title);
$PAGE->set_heading($title);
$providerclass = "\\$provider\\provider";
$mform = $providerclass::get_action_settings($action, $data);
$mform = $providerclass::get_action_settings($action, $customdata);
if ($mform->is_cancelled()) {
$data = $mform->get_data();

View file

@ -75,8 +75,6 @@ class action_generate_image_form extends action_settings_form {
$mform->addElement('hidden', 'providerid', $providerid);
$mform->setType('providerid', PARAM_INT);
$this->add_action_buttons();
$this->set_data($actionconfig);
}
}

View file

@ -86,8 +86,6 @@ class action_generate_text_form extends action_settings_form {
$mform->addElement('hidden', 'providerid', $providerid);
$mform->setType('providerid', PARAM_INT);
$this->add_action_buttons();
$this->set_data($actionconfig);
}
}

View file

@ -0,0 +1,11 @@
define("aiprovider_openai/modelchooser",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* AI provider model selection handler.
*
* @module aiprovider_openai/modelchooser
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Selectors_fields={selector:'[data-modelchooser-field="selector"]',updateButton:'[data-modelchooser-field="updateButton"]'};_exports.init=()=>{const modelSelector=document.querySelector(Selectors_fields.selector);modelSelector&&modelSelector.addEventListener("change",(e=>{modelSelector.options[e.target.selectedIndex].selected=!0;e.target.closest("form").querySelector(Selectors_fields.updateButton).click()}))}}));
//# sourceMappingURL=modelchooser.min.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"modelchooser.min.js","sources":["../src/modelchooser.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/ //\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * AI provider model selection handler.\n *\n * @module aiprovider_openai/modelchooser\n * @copyright 2025 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst Selectors = {\n fields: {\n selector: '[data-modelchooser-field=\"selector\"]',\n updateButton: '[data-modelchooser-field=\"updateButton\"]',\n },\n};\n\n/**\n * Initialise the AI provider chooser.\n */\nexport const init = () => {\n const modelSelector = document.querySelector(Selectors.fields.selector);\n if (modelSelector) {\n modelSelector.addEventListener('change', e => {\n modelSelector.options[e.target.selectedIndex].selected = true;\n const form = e.target.closest('form');\n const updateButton = form.querySelector(Selectors.fields.updateButton);\n updateButton.click();\n });\n }\n};\n"],"names":["Selectors","selector","updateButton","modelSelector","document","querySelector","addEventListener","e","options","target","selectedIndex","selected","closest","click"],"mappings":";;;;;;;;MAsBMA,iBACM,CACJC,SAAU,uCACVC,aAAc,0DAOF,WACVC,cAAgBC,SAASC,cAAcL,iBAAiBC,UAC1DE,eACAA,cAAcG,iBAAiB,UAAUC,IACrCJ,cAAcK,QAAQD,EAAEE,OAAOC,eAAeC,UAAW,EAC5CJ,EAAEE,OAAOG,QAAQ,QACJP,cAAcL,iBAAiBE,cAC5CW"}

View file

@ -0,0 +1,43 @@
// 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/>.
/**
* AI provider model selection handler.
*
* @module aiprovider_openai/modelchooser
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Selectors = {
fields: {
selector: '[data-modelchooser-field="selector"]',
updateButton: '[data-modelchooser-field="updateButton"]',
},
};
/**
* Initialise the AI provider chooser.
*/
export const init = () => {
const modelSelector = document.querySelector(Selectors.fields.selector);
if (modelSelector) {
modelSelector.addEventListener('change', e => {
modelSelector.options[e.target.selectedIndex].selected = true;
const form = e.target.closest('form');
const updateButton = form.querySelector(Selectors.fields.updateButton);
updateButton.click();
});
}
};

View file

@ -51,6 +51,32 @@ abstract class abstract_processor extends process_base {
return $this->provider->actionconfig[$this->action::class]['settings']['model'];
}
/**
* Get the model settings.
*
* @return array
*/
protected function get_model_settings(): array {
$settings = $this->provider->actionconfig[$this->action::class]['settings'];
if (!empty($settings['modelextraparams'])) {
// Custom model settings.
$params = json_decode($settings['modelextraparams'], true);
foreach ($params as $key => $param) {
$settings[$key] = $param;
}
}
// Unset unnecessary settings.
unset(
$settings['model'],
$settings['endpoint'],
$settings['systeminstruction'],
$settings['providerid'],
$settings['modelextraparams'],
);
return $settings;
}
/**
* Get the system instructions.
*

View file

@ -0,0 +1,49 @@
<?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\aimodel;
use core_ai\aimodel\base;
/**
* DALL-e-3 AI model.
*
* @package aiprovider_openai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dalle3 extends base implements openai_base {
#[\Override]
public function get_model_name(): string {
return 'dall-e-3';
}
#[\Override]
public function get_model_display_name(): string {
return 'DALL-e-3';
}
#[\Override]
public function has_model_settings(): bool {
return false;
}
#[\Override]
public function model_type(): int {
return self::MODEL_TYPE_IMAGE;
}
}

View file

@ -0,0 +1,87 @@
<?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\aimodel;
use core_ai\aimodel\base;
use MoodleQuickForm;
/**
* GPT-4o AI model.
*
* @package aiprovider_openai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class gpt4o extends base implements openai_base {
#[\Override]
public function get_model_name(): string {
return 'gpt-4o';
}
#[\Override]
public function get_model_display_name(): string {
return 'GPT-4o';
}
#[\Override]
public function has_model_settings(): bool {
return true;
}
#[\Override]
public function add_model_settings(MoodleQuickForm $mform): void {
$mform->addElement(
'text',
'top_p',
get_string('settings_top_p', 'aiprovider_openai'),
);
$mform->setType('top_p', PARAM_FLOAT);
$mform->addHelpButton('top_p', 'settings_top_p', 'aiprovider_openai');
$mform->addElement(
'text',
'max_tokens',
get_string('settings_max_tokens', 'aiprovider_openai'),
);
$mform->setType('max_tokens', PARAM_INT);
$mform->addHelpButton('max_tokens', 'settings_max_tokens', 'aiprovider_openai');
$mform->addElement(
'text',
'frequency_penalty',
get_string('settings_frequency_penalty', 'aiprovider_openai'),
);
// This is a raw value because it can be a float from -2.0 to 2.0.
$mform->setType('frequency_penalty', PARAM_RAW);
$mform->addHelpButton('frequency_penalty', 'settings_frequency_penalty', 'aiprovider_openai');
$mform->addElement(
'text',
'presence_penalty',
get_string('settings_presence_penalty', 'aiprovider_openai'),
);
// This is a raw value because it can be a float from -2.0 to 2.0.
$mform->setType('presence_penalty', PARAM_RAW);
$mform->addHelpButton('presence_penalty', 'settings_presence_penalty', 'aiprovider_openai');
}
#[\Override]
public function model_type(): int {
return self::MODEL_TYPE_TEXT;
}
}

View file

@ -0,0 +1,47 @@
<?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\aimodel;
/**
* O1 AI model.
*
* @package aiprovider_openai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class o1 extends gpt4o {
#[\Override]
public function get_model_name(): string {
return 'o1';
}
#[\Override]
public function get_model_display_name(): string {
return 'O1';
}
#[\Override]
public function has_model_settings(): bool {
return true;
}
#[\Override]
public function model_type(): int {
return self::MODEL_TYPE_TEXT;
}
}

View file

@ -0,0 +1,39 @@
<?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\aimodel;
/**
* OpenAI base AI model interface.
*
* @package aiprovider_openai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface openai_base {
/** @var int MODEL_TYPE_TEXT Text model type. */
public const MODEL_TYPE_TEXT = 1;
/** @var int MODEL_TYPE_IMAGE Image model type. */
public const MODEL_TYPE_IMAGE = 2;
/**
* Get model type.
*
* @return int Model type.
*/
public function model_type(): int;
}

View file

@ -0,0 +1,198 @@
<?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\form;
use aiprovider_openai\helper;
use core_ai\form\action_settings_form;
/**
* Base action settings form for OpenAI provider.
*
* @package aiprovider_openai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class action_form extends action_settings_form {
/**
* @var array Action configuration.
*/
protected array $actionconfig;
/**
* @var string|null Return URL.
*/
protected ?string $returnurl;
/**
* @var string Action name.
*/
protected string $actionname;
/**
* @var string Action class.
*/
protected string $action;
/**
* @var int Provider ID.
*/
protected int $providerid;
/**
* @var string Provider name.
*/
protected string $providername;
#[\Override]
protected function definition(): void {
$mform = $this->_form;
$this->actionconfig = $this->_customdata['actionconfig']['settings'] ?? [];
$this->returnurl = $this->_customdata['returnurl'] ?? null;
$this->actionname = $this->_customdata['actionname'];
$this->action = $this->_customdata['action'];
$this->providerid = $this->_customdata['providerid'] ?? 0;
$this->providername = $this->_customdata['providername'] ?? 'aiprovider_openai';
$mform->addElement('header', 'generalsettingsheader', get_string('general', 'core'));
}
#[\Override]
public function set_data($data): void {
if (!empty($data['modelextraparams'])) {
$data['modelextraparams'] = json_encode(json_decode($data['modelextraparams']), JSON_PRETTY_PRINT);
}
parent::set_data($data);
}
#[\Override]
public function get_data(): ?\stdClass {
$data = parent::get_data();
if ($data) {
if (isset($data->modeltemplate)) {
if ($data->modeltemplate === 'custom') {
$data->model = $data->custommodel;
} else {
// Set the model to the selected model template.
$data->model = $data->modeltemplate;
}
}
// Unset the model template.
unset($data->custommodel);
unset($data->modeltemplate);
// Unset any false-y values.
$data = (object) array_filter((array) $data);
}
return $data;
}
#[\Override]
public function validation($data, $files): array {
$errors = parent::validation($data, $files);
// Validate the extra parameters.
if (!empty($data['modelextraparams'])) {
json_decode($data['modelextraparams']);
if (json_last_error() !== JSON_ERROR_NONE) {
$errors['modelextraparams'] = get_string('invalidjson', 'aiprovider_openai');
}
}
// Validate the model.
if ($data['modeltemplate'] === 'custom' && empty($data['custommodel'])) {
$errors['custommodel'] = get_string('required');
}
return $errors;
}
#[\Override]
public function get_defaults(): array {
$data = parent::get_defaults();
unset(
$data['modeltemplate'],
$data['custommodel'],
$data['modelextraparams'],
);
return $data;
}
/**
* Add model fields to the form.
*
* @param int $modeltype Model type.
*/
protected function add_model_fields(int $modeltype): void {
global $PAGE;
$PAGE->requires->js_call_amd('aiprovider_openai/modelchooser', 'init');
$mform = $this->_form;
// Action model to use.
$mform->addElement(
'select',
'modeltemplate',
get_string("action:{$this->actionname}:model", 'aiprovider_openai'),
$this->get_model_list($modeltype),
['data-modelchooser-field' => 'selector'],
);
$mform->setType('modeltemplate', PARAM_TEXT);
$mform->addRule('modeltemplate', null, 'required', null, 'client');
if (!empty($this->actionconfig['model']) &&
(!array_key_exists($this->actionconfig['model'], $this->get_model_list($modeltype)) ||
!empty($this->actionconfig['modelextraparams']))) {
$defaultmodel = 'custom';
} else {
$defaultmodel = $this->actionconfig['model'] ?? 'gpt-4o';
}
$mform->setDefault('modeltemplate', $defaultmodel);
$mform->addHelpButton('modeltemplate', "action:{$this->actionname}:model", 'aiprovider_openai');
$mform->addElement('hidden', 'model', $this->actionconfig['model'] ?? 'gpt-4o');
$mform->setType('model', PARAM_TEXT);
$mform->addElement('text', 'custommodel', get_string('custom_model_name', 'aiprovider_openai'));
$mform->setType('custommodel', PARAM_TEXT);
$mform->setDefault('custommodel', $this->actionconfig['model'] ?? '');
$mform->hideIf('custommodel', 'modeltemplate', 'neq', 'custom');
$mform->registerNoSubmitButton('updateactionsettings');
$mform->addElement(
'submit',
'updateactionsettings',
'updateactionsettings',
['data-modelchooser-field' => 'updateButton', 'class' => 'd-none']
);
}
/**
* Get the list of models.
*
* @param int $modeltype Model type.
* @return array List of models.
*/
protected function get_model_list(int $modeltype): array {
$models = [];
$models['custom'] = get_string('custom', 'core_form');
foreach (helper::get_model_classes() as $class) {
$model = new $class();
if ($model->model_type() == $modeltype) {
$models[$model->get_model_name()] = $model->get_model_display_name();
}
}
return $models;
}
}

View file

@ -16,7 +16,7 @@
namespace aiprovider_openai\form;
use core_ai\form\action_settings_form;
use aiprovider_openai\aimodel\openai_base;
/**
* Generate image action provider settings form.
@ -25,58 +25,42 @@ use core_ai\form\action_settings_form;
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class action_generate_image_form extends action_settings_form {
class action_generate_image_form extends action_form {
#[\Override]
protected function definition() {
protected function definition(): void {
parent::definition();
$mform = $this->_form;
$actionconfig = $this->_customdata['actionconfig']['settings'] ?? [];
$returnurl = $this->_customdata['returnurl'] ?? null;
$actionname = $this->_customdata['actionname'];
$action = $this->_customdata['action'];
$providerid = $this->_customdata['providerid'] ?? 0;
// Action model to use.
$mform->addElement(
'text',
'model',
get_string("action:{$actionname}:model", 'aiprovider_openai'),
'maxlength="255" size="20"',
);
$mform->setType('model', PARAM_TEXT);
$mform->addRule('model', null, 'required', null, 'client');
$mform->setDefault('model', $actionconfig['model'] ?? 'dall-e-3');
$mform->addHelpButton('model', "action:{$actionname}:model", 'aiprovider_openai');
$this->add_model_fields(openai_base::MODEL_TYPE_IMAGE);
// API endpoint.
$mform->addElement(
'text',
'endpoint',
get_string("action:{$actionname}:endpoint", 'aiprovider_openai'),
get_string("action:{$this->actionname}:endpoint", 'aiprovider_openai'),
'maxlength="255" size="30"',
);
$mform->setType('endpoint', PARAM_URL);
$mform->addRule('endpoint', null, 'required', null, 'client');
$mform->setDefault('endpoint', $actionconfig['endpoint'] ?? 'https://api.openai.com/v1/images/generations');
$mform->setDefault('endpoint', $this->actionconfig['endpoint'] ?? 'https://api.openai.com/v1/images/generations');
if ($returnurl) {
$mform->addElement('hidden', 'returnurl', $returnurl);
if ($this->returnurl) {
$mform->addElement('hidden', 'returnurl', $this->returnurl);
$mform->setType('returnurl', PARAM_LOCALURL);
}
// Add the action class as a hidden field.
$mform->addElement('hidden', 'action', $action);
$mform->addElement('hidden', 'action', $this->action);
$mform->setType('action', PARAM_TEXT);
// Add the provider class as a hidden field.
$mform->addElement('hidden', 'provider', 'aiprovider_openai');
$mform->addElement('hidden', 'provider', $this->providername);
$mform->setType('provider', PARAM_TEXT);
// Add the provider id as a hidden field.
$mform->addElement('hidden', 'providerid', $providerid);
$mform->addElement('hidden', 'providerid', $this->providerid);
$mform->setType('providerid', PARAM_INT);
$this->add_action_buttons();
$this->set_data($actionconfig);
$this->set_data($this->actionconfig);
}
}

View file

@ -16,7 +16,7 @@
namespace aiprovider_openai\form;
use core_ai\form\action_settings_form;
use aiprovider_openai\aimodel\openai_base;
/**
* Generate text action provider settings form.
@ -25,33 +25,19 @@ use core_ai\form\action_settings_form;
* @copyright 2024 Matt Porritt <matt.porritt@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class action_generate_text_form extends action_settings_form {
class action_generate_text_form extends action_form {
#[\Override]
protected function definition() {
protected function definition(): void {
parent::definition();
$mform = $this->_form;
$actionconfig = $this->_customdata['actionconfig']['settings'] ?? [];
$returnurl = $this->_customdata['returnurl'] ?? null;
$actionname = $this->_customdata['actionname'];
$action = $this->_customdata['action'];
$providerid = $this->_customdata['providerid'] ?? 0;
// Action model to use.
$mform->addElement(
'text',
'model',
get_string("action:{$actionname}:model", 'aiprovider_openai'),
'maxlength="255" size="20"',
);
$mform->setType('model', PARAM_TEXT);
$mform->addRule('model', null, 'required', null, 'client');
$mform->setDefault('model', $actionconfig['model'] ?? 'gpt-4o');
$mform->addHelpButton('model', "action:{$actionname}:model", 'aiprovider_openai');
$this->add_model_fields(openai_base::MODEL_TYPE_TEXT);
// API endpoint.
$mform->addElement(
'text',
'endpoint',
get_string("action:{$actionname}:endpoint", 'aiprovider_openai'),
get_string("action:{$this->actionname}:endpoint", 'aiprovider_openai'),
'maxlength="255" size="30"',
);
$mform->setType('endpoint', PARAM_URL);
@ -62,32 +48,31 @@ class action_generate_text_form extends action_settings_form {
$mform->addElement(
'textarea',
'systeminstruction',
get_string("action:{$actionname}:systeminstruction", 'aiprovider_openai'),
get_string("action:{$this->actionname}:systeminstruction", 'aiprovider_openai'),
'wrap="virtual" rows="5" cols="20"',
);
$mform->setType('systeminstruction', PARAM_TEXT);
$mform->setDefault('systeminstruction', $actionconfig['systeminstruction'] ?? $action::get_system_instruction());
$mform->addHelpButton('systeminstruction', "action:{$actionname}:systeminstruction", 'aiprovider_openai');
$mform->setDefault('systeminstruction', $actionconfig['systeminstruction'] ?? $this->action::get_system_instruction());
$mform->addHelpButton('systeminstruction', "action:{$this->actionname}:systeminstruction", 'aiprovider_openai');
if ($returnurl) {
$mform->addElement('hidden', 'returnurl', $returnurl);
if ($this->returnurl) {
$mform->addElement('hidden', 'returnurl', $this->returnurl);
$mform->setType('returnurl', PARAM_LOCALURL);
}
// Add the action class as a hidden field.
$mform->addElement('hidden', 'action', $action);
$mform->addElement('hidden', 'action', $this->action);
$mform->setType('action', PARAM_TEXT);
// Add the provider class as a hidden field.
$mform->addElement('hidden', 'provider', 'aiprovider_openai');
$mform->addElement('hidden', 'provider', $this->providername);
$mform->setType('provider', PARAM_TEXT);
// Add the provider id as a hidden field.
$mform->addElement('hidden', 'providerid', $providerid);
$mform->addElement('hidden', 'providerid', $this->providerid);
$mform->setType('providerid', PARAM_INT);
$this->add_action_buttons();
$this->set_data($actionconfig);
$this->set_data($this->actionconfig);
}
}

View file

@ -0,0 +1,62 @@
<?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\aimodel\base;
/**
* Helper class for the OpenAI provider.
*
* @package aiprovider_openai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* Get all model classes.
*
* @return array Array of model classes.
*/
public static function get_model_classes(): array {
$models = [];
$modelclasses = \core_component::get_component_classes_in_namespace('aiprovider_openai', 'aimodel');
foreach ($modelclasses as $class => $path) {
if (!class_exists($class) || !is_a($class, base::class, true)) {
throw new \coding_exception("Model class not valid: {$class}");
}
$models[] = $class;
}
return $models;
}
/**
* Get model class by name.
*
* @param string $modelname Model name.
* @return base|null
*/
public static function get_model_class(string $modelname): ?base {
foreach (static::get_model_classes() as $classname) {
$model = new $classname();
if ($model->get_model_name() === $modelname) {
return $model;
}
}
return null;
}
}

View file

@ -16,6 +16,8 @@
namespace aiprovider_openai;
use aiprovider_openai\model\base;
use core_ai\hook\after_ai_action_settings_form_hook;
use core_ai\hook\after_ai_provider_form_hook;
/**
@ -61,4 +63,44 @@ class hook_listener {
}
/**
* Hook listener for the Open AI action settings form.
*
* @param after_ai_action_settings_form_hook $hook The hook to add to config action settings.
*/
public static function set_model_form_definition_for_aiprovider_openai(after_ai_action_settings_form_hook $hook): void {
if ($hook->plugin !== 'aiprovider_openai') {
return;
}
$mform = $hook->mform;
if (isset($mform->_elementIndex['modeltemplate'])) {
$model = $mform->getElementValue('modeltemplate');
if (is_array($model)) {
$model = $model[0];
}
if ($model == 'custom') {
$mform->addElement('header', 'modelsettingsheader', get_string('settings', 'aiprovider_openai'));
$mform->addElement('html', get_string('settings_help', 'aiprovider_openai'));
$mform->addElement(
'textarea',
'modelextraparams',
get_string('extraparams', 'aiprovider_openai'),
['rows' => 5, 'cols' => 20],
);
$mform->setType('modelextraparams', PARAM_TEXT);
$mform->addElement('static', 'modelextraparams_help', null, get_string('extraparams_help', 'aiprovider_openai'));
} else {
$targetmodel = helper::get_model_class($model);
if ($targetmodel) {
if ($targetmodel->has_model_settings()) {
$mform->addElement('header', 'modelsettingsheader', get_string('settings', 'aiprovider_openai'));
$mform->addElement('html', get_string('settings_help', 'aiprovider_openai'));
$targetmodel->add_model_settings($mform);
}
}
}
}
}
}

View file

@ -75,22 +75,28 @@ class process_generate_image extends abstract_processor {
#[\Override]
protected function create_request_object(string $userid): RequestInterface {
// Create the request object.
$requestobj = new \stdClass();
$requestobj->model = $this->get_model();
$requestobj->user = $userid;
$requestobj->prompt = $this->action->get_configuration('prompttext');
$requestobj->n = $this->numberimages;
$requestobj->quality = $this->action->get_configuration('quality');
$requestobj->response_format = $this->responseformat;
$requestobj->size = $this->calculate_size($this->action->get_configuration('aspectratio'));
$requestobj->style = $this->action->get_configuration('style');
// Append the extra model settings.
$modelsettings = $this->get_model_settings();
foreach ($modelsettings as $setting => $value) {
$requestobj->$setting = $value;
}
return new Request(
method: 'POST',
uri: '',
headers: [
'Content-Type' => 'application/json',
],
body: json_encode((object) [
'prompt' => $this->action->get_configuration('prompttext'),
'model' => $this->get_model(),
'n' => $this->numberimages,
'quality' => $this->action->get_configuration('quality'),
'response_format' => $this->responseformat,
'size' => $this->calculate_size($this->action->get_configuration('aspectratio')),
'style' => $this->action->get_configuration('style'),
'user' => $userid,
]),
uri: '',
headers: [
'Content-Type' => 'application/json',
],
body: json_encode($requestobj),
);
}

View file

@ -57,6 +57,12 @@ class process_generate_text extends abstract_processor {
$requestobj->messages = [$userobj];
}
// Append the extra model settings.
$modelsettings = $this->get_model_settings();
foreach ($modelsettings as $setting => $value) {
$requestobj->$setting = $value;
}
return new Request(
method: 'POST',
uri: '',

View file

@ -94,6 +94,7 @@ class provider extends \core_ai\provider {
$customdata = [
'actionname' => $actionname,
'action' => $action,
'providername' => 'aiprovider_openai',
];
if ($actionname === 'generate_text' || $actionname === 'summarise_text') {
$mform = new form\action_generate_text_form(customdata: $customdata);

View file

@ -0,0 +1,66 @@
<?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\test;
/**
* Trait for test cases.
*
* @package aiprovider_openai
* @copyright 2025 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait testcase_helper_trait {
/**
* Create the provider object.
*
* @param string $actionclass The action class to use.
* @param array $actionconfig The action configuration to use.
*/
public function create_provider(
string $actionclass,
array $actionconfig = [],
): \core_ai\provider {
$manager = \core\di::get(\core_ai\manager::class);
$config = [
'apikey' => '123',
'enableuserratelimit' => true,
'userratelimit' => 1,
'enableglobalratelimit' => true,
'globalratelimit' => 1,
];
$defaultactionconfig = [
$actionclass => [
'settings' => [
'model' => 'gpt-4o',
'endpoint' => "https://api.openai.com/v1/chat/completions",
],
],
];
foreach ($actionconfig as $key => $value) {
$defaultactionconfig[$actionclass]['settings'][$key] = $value;
}
$provider = $manager->create_provider_instance(
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
actionconfig: $defaultactionconfig,
);
return $provider;
}
}

View file

@ -29,4 +29,9 @@ $callbacks = [
'hook' => \core_ai\hook\after_ai_provider_form_hook::class,
'callback' => \aiprovider_openai\hook_listener::class . '::set_form_definition_for_aiprovider_openai',
],
[
'hook' => \core_ai\hook\after_ai_action_settings_form_hook::class,
'callback' => \aiprovider_openai\hook_listener::class . '::set_model_form_definition_for_aiprovider_openai',
],
];

View file

@ -37,6 +37,16 @@ $string['action:summarise_text:systeminstruction'] = 'System instruction';
$string['action:summarise_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.';
$string['apikey'] = 'OpenAI API key';
$string['apikey_help'] = 'Get a key from your <a href="https://platform.openai.com/account/api-keys" target="_blank">OpenAI API keys</a>.';
$string['custom_model_name'] = 'Custom model name';
$string['extraparams'] = 'Extra parameters';
$string['extraparams_help'] = 'Extra parameters can be configured here. We support JSON format. For example:
<pre>
{
"temperature": 0.5,
"max_tokens": 100
}
</pre>';
$string['invalidjson'] = 'Invalid JSON string';
$string['orgid'] = 'OpenAI organization ID';
$string['orgid_help'] = 'Get your OpenAI organization ID from your <a href="https://platform.openai.com/account/org-settings" target="_blank">OpenAI account</a>.';
$string['pluginname'] = 'OpenAI API provider';
@ -46,6 +56,16 @@ $string['privacy:metadata:aiprovider_openai:model'] = 'The model used to generat
$string['privacy:metadata:aiprovider_openai:numberimages'] = 'When generating images the number of images used in the response.';
$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['settings'] = 'Settings';
$string['settings_frequency_penalty'] = 'frequency_penalty';
$string['settings_frequency_penalty_help'] = 'Penalizes new tokens based on their frequency in the text so far';
$string['settings_help'] = 'You can adjust the settings below to customize how requests are sent to OpenAI. Update the values as needed, ensuring they align with your requirements.<br><br>';
$string['settings_max_tokens'] = 'max_tokens';
$string['settings_max_tokens_help'] = 'The maximum number of tokens to generate in the response';
$string['settings_presence_penalty'] = 'presence_penalty';
$string['settings_presence_penalty_help'] = 'Penalizes new tokens based on whether they appear in the text so far';
$string['settings_top_p'] = 'top_p';
$string['settings_top_p_help'] = 'Controls nucleus sampling';
// Deprecated since Moodle 5.0.
$string['action:generate_image:model_desc'] = 'The model used to generate images from user prompts.';

View file

@ -16,6 +16,7 @@
namespace aiprovider_openai;
use aiprovider_openai\test\testcase_helper_trait;
use core_ai\aiactions\base;
use core_ai\provider;
use GuzzleHttp\Psr7\Response;
@ -31,6 +32,9 @@ use GuzzleHttp\Psr7\Response;
* @covers \aiprovider_openai\abstract_processor
*/
final class process_generate_image_test extends \advanced_testcase {
use testcase_helper_trait;
/** @var string A successful response in JSON format. */
protected string $responsebodyjson;
@ -51,27 +55,14 @@ final class process_generate_image_test extends \advanced_testcase {
$this->resetAfterTest();
// Load a response body from a file.
$this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_openai', 'image_request_success.json'));
$this->create_provider();
$this->create_action();
}
/**
* Create the provider object.
*/
private function create_provider(): void {
$this->manager = \core\di::get(\core_ai\manager::class);
$config = [
'apikey' => '123',
'enableuserratelimit' => true,
'userratelimit' => 1,
'enableglobalratelimit' => true,
'globalratelimit' => 1,
];
$this->provider = $this->manager->create_provider_instance(
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\generate_image::class,
actionconfig: [
'model' => 'dall-e-3',
],
);
$this->create_action();
}
/**
@ -132,6 +123,50 @@ final class process_generate_image_test extends \advanced_testcase {
$this->assertEquals('1024x1024', $requestdata->size);
}
/**
* Test create_request_object with extra model settings.
*/
public function test_create_request_object_with_model_settings(): void {
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\generate_image::class,
actionconfig: [
'model' => 'dall-e-3',
'temperature' => '0.5',
'max_tokens' => '100',
],
);
$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);
$body = (object) json_decode($request->getBody()->getContents());
$this->assertEquals('dall-e-3', $body->model);
$this->assertEquals('0.5', $body->temperature);
$this->assertEquals('100', $body->max_tokens);
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\generate_image::class,
actionconfig: [
'model' => 'my-custom-gpt',
'modelextraparams' => '{"temperature": 0.5,"max_tokens": 100}',
],
);
$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);
$body = (object) json_decode($request->getBody()->getContents());
$this->assertEquals('my-custom-gpt', $body->model);
$this->assertEquals('0.5', $body->temperature);
$this->assertEquals('100', $body->max_tokens);
}
/**
* Test the API error response handler method.
*/
@ -397,6 +432,14 @@ final class process_generate_image_test extends \advanced_testcase {
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
actionconfig: [
\core_ai\aiactions\generate_image::class => [
'settings' => [
'model' => 'dall-e-3',
'endpoint' => "https://api.openai.com/v1/chat/completions",
],
],
],
);
// Mock the http client to return a successful response.
@ -538,6 +581,14 @@ final class process_generate_image_test extends \advanced_testcase {
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
actionconfig: [
\core_ai\aiactions\generate_image::class => [
'settings' => [
'model' => 'dall-e-3',
'endpoint' => "https://api.openai.com/v1/chat/completions",
],
],
],
);
// Mock the http client to return a successful response.
@ -592,7 +643,7 @@ final class process_generate_image_test extends \advanced_testcase {
['Content-Type' => 'image/jpeg'],
\GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
));
$this->create_provider();
$this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class);
$this->create_action($user1->id);
$processor = new process_generate_image($provider, $this->action);
$result = $processor->process();
@ -603,7 +654,7 @@ final class process_generate_image_test extends \advanced_testcase {
// Case 3: Global rate limit has been reached for a different user too.
// Log in user2.
$this->setUser($user2);
$this->create_provider();
$this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class);
$this->create_action($user2->id);
// The response from OpenAI.
$mock->append(new Response(
@ -653,7 +704,7 @@ final class process_generate_image_test extends \advanced_testcase {
['Content-Type' => 'image/jpeg'],
\GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
));
$this->create_provider();
$this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class);
$this->create_action($user1->id);
$processor = new process_generate_image($provider, $this->action);
$result = $processor->process();

View file

@ -16,7 +16,7 @@
namespace aiprovider_openai;
use aiprovider_openai\process_generate_text;
use aiprovider_openai\test\testcase_helper_trait;
use core_ai\aiactions\base;
use core_ai\provider;
use GuzzleHttp\Psr7\Response;
@ -32,6 +32,9 @@ use GuzzleHttp\Psr7\Response;
* @covers \aiprovider_openai\abstract_processor
*/
final class process_generate_text_test extends \advanced_testcase {
use testcase_helper_trait;
/** @var string A successful response in JSON format. */
protected string $responsebodyjson;
@ -52,27 +55,14 @@ final class process_generate_text_test extends \advanced_testcase {
$this->resetAfterTest();
// Load a response body from a file.
$this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_openai', 'text_request_success.json'));
$this->create_provider();
$this->create_action();
}
/**
* Create the provider object.
*/
private function create_provider(): void {
$this->manager = \core\di::get(\core_ai\manager::class);
$config = [
'apikey' => '123',
'enableuserratelimit' => true,
'userratelimit' => 1,
'enableglobalratelimit' => true,
'globalratelimit' => 1,
];
$this->provider = $this->manager->create_provider_instance(
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\generate_text::class,
actionconfig: [
'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
],
);
$this->create_action();
}
/**
@ -103,6 +93,51 @@ final class process_generate_text_test extends \advanced_testcase {
$this->assertEquals('user', $body->messages[1]->role);
}
/**
* Test create_request_object with extra model settings.
*/
public function test_create_request_object_with_model_settings(): void {
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\generate_text::class,
actionconfig: [
'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
'temperature' => '0.5',
'max_tokens' => '100',
],
);
$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('gpt-4o', $body->model);
$this->assertEquals('0.5', $body->temperature);
$this->assertEquals('100', $body->max_tokens);
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\generate_text::class,
actionconfig: [
'model' => 'my-custom-gpt',
'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
'modelextraparams' => '{"temperature": 0.5,"max_tokens": 100}',
],
);
$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('my-custom-gpt', $body->model);
$this->assertEquals('0.5', $body->temperature);
$this->assertEquals('100', $body->max_tokens);
}
/**
* Test the API error response handler method.
*/
@ -325,6 +360,15 @@ final class process_generate_text_test extends \advanced_testcase {
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
actionconfig: [
\core_ai\aiactions\generate_text::class => [
'settings' => [
'model' => 'gpt-4o',
'endpoint' => "https://api.openai.com/v1/chat/completions",
'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
],
],
],
);
// Mock the http client to return a successful response.
@ -381,7 +425,7 @@ final class process_generate_text_test extends \advanced_testcase {
['Content-Type' => 'application/json'],
$this->responsebodyjson,
));
$this->create_provider();
$this->provider = $this->create_provider(\core_ai\aiactions\generate_text::class);
$this->create_action($user1->id);
$processor = new process_generate_text($provider, $this->action);
$result = $processor->process();
@ -410,6 +454,15 @@ final class process_generate_text_test extends \advanced_testcase {
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
actionconfig: [
\core_ai\aiactions\generate_text::class => [
'settings' => [
'model' => 'gpt-4o',
'endpoint' => "https://api.openai.com/v1/chat/completions",
'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
],
],
],
);
// Mock the http client to return a successful response.
@ -466,7 +519,7 @@ final class process_generate_text_test extends \advanced_testcase {
['Content-Type' => 'application/json'],
$this->responsebodyjson,
));
$this->create_provider();
$this->provider = $this->create_provider(\core_ai\aiactions\generate_text::class);
$this->create_action($user1->id);
$processor = new process_generate_text($provider, $this->action);
$result = $processor->process();

View file

@ -16,7 +16,7 @@
namespace aiprovider_openai;
use aiprovider_openai\process_summarise_text;
use aiprovider_openai\test\testcase_helper_trait;
use core_ai\aiactions\base;
use core_ai\provider;
use GuzzleHttp\Psr7\Response;
@ -32,6 +32,9 @@ use GuzzleHttp\Psr7\Response;
* @covers \aiprovider_openai\abstract_processor
*/
final class process_summarise_text_test extends \advanced_testcase {
use testcase_helper_trait;
/** @var string A successful response in JSON format. */
protected string $responsebodyjson;
@ -52,27 +55,14 @@ final class process_summarise_text_test extends \advanced_testcase {
$this->resetAfterTest();
// Load a response body from a file.
$this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_openai', 'text_request_success.json'));
$this->create_provider();
$this->create_action();
}
/**
* Create the provider object.
*/
private function create_provider(): void {
$this->manager = \core\di::get(\core_ai\manager::class);
$config = [
'apikey' => '123',
'enableuserratelimit' => true,
'userratelimit' => 1,
'enableglobalratelimit' => true,
'globalratelimit' => 1,
];
$this->provider = $this->manager->create_provider_instance(
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\summarise_text::class,
actionconfig: [
'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
],
);
$this->create_action();
}
/**
@ -105,6 +95,51 @@ final class process_summarise_text_test extends \advanced_testcase {
$this->assertEquals('user', $body->messages[1]->role);
}
/**
* Test create_request_object with extra model settings.
*/
public function test_create_request_object_with_model_settings(): void {
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\summarise_text::class,
actionconfig: [
'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
'temperature' => '0.5',
'max_tokens' => '100',
],
);
$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('gpt-4o', $body->model);
$this->assertEquals('0.5', $body->temperature);
$this->assertEquals('100', $body->max_tokens);
$this->provider = $this->create_provider(
actionclass: \core_ai\aiactions\summarise_text::class,
actionconfig: [
'model' => 'my-custom-gpt',
'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
'modelextraparams' => '{"temperature": 0.5,"max_tokens": 100}',
],
);
$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('my-custom-gpt', $body->model);
$this->assertEquals('0.5', $body->temperature);
$this->assertEquals('100', $body->max_tokens);
}
/**
* Test the API error response handler method.
*
@ -319,6 +354,15 @@ final class process_summarise_text_test extends \advanced_testcase {
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
actionconfig: [
\core_ai\aiactions\summarise_text::class => [
'settings' => [
'model' => 'gpt-4o',
'endpoint' => "https://api.openai.com/v1/chat/completions",
'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
],
],
],
);
// Mock the http client to return a successful response.
@ -403,6 +447,15 @@ final class process_summarise_text_test extends \advanced_testcase {
classname: '\aiprovider_openai\provider',
name: 'dummy',
config: $config,
actionconfig: [
\core_ai\aiactions\summarise_text::class => [
'settings' => [
'model' => 'gpt-4o',
'endpoint' => "https://api.openai.com/v1/chat/completions",
'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
],
],
],
);
// Mock the http client to return a successful response.

View file

@ -64,9 +64,10 @@ Feature: An administrator can manage AI subsystem settings
And I should see "Configure provider instance"
And I click on the "Settings" link in the table row containing "Generate text"
And I should see "Generate text action settings"
And I set the field "AI model" to "Custom"
And I set the following fields to these values:
| AI model | gpt-3 |
| API endpoint | https://api.openai.com/v1/engines/gpt-3/completions |
| Custom model name | gpt-3 |
| API endpoint | https://api.openai.com/v1/engines/gpt-3/completions |
And I click on "Save changes" "button"
Then I should see "Generate text action settings updated"