MDL-83006 AI: Provider Plugin - Ollama
Make Ollama compatible with new features: - AI Provider instance - AI Model settings - Behat tests
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
namespace core_ai;
|
||||
|
||||
use core_ai\form\action_settings_form;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Spatie\Cloneable\Cloneable;
|
||||
|
||||
/**
|
||||
@@ -187,6 +188,32 @@ abstract class provider {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a request to add any headers required by the provider (if needed).
|
||||
* AI providers will need to override this method to add their own headers.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public function add_authentication_headers(RequestInterface $request): RequestInterface {
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 the AI provider.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this object to a stdClass, suitable for saving to the database.
|
||||
*
|
||||
|
||||
@@ -41,27 +41,7 @@ class provider extends \core_ai\provider {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a user id.
|
||||
*
|
||||
* This is a hash of the site id and user id,
|
||||
* this means we can determine who made the request
|
||||
* but don't pass any personal data to AzureAI.
|
||||
*
|
||||
* @param string $userid The user id.
|
||||
* @return string The generated user id.
|
||||
*/
|
||||
public function generate_userid(string $userid): string {
|
||||
global $CFG;
|
||||
return hash('sha256', $CFG->siteidentifier . $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a request to add any headers required by the provider.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return RequestInterface
|
||||
*/
|
||||
#[\Override]
|
||||
public function add_authentication_headers(RequestInterface $request): RequestInterface {
|
||||
return $request
|
||||
->withAddedHeader('api-key', $this->config['apikey']);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
define("aiprovider_ollama/modelchooser",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
|
||||
/**
|
||||
* AI provider model selection handler.
|
||||
*
|
||||
* @module aiprovider_ollama/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
|
||||
@@ -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_ollama/modelchooser\n * @copyright 2025 Huong Nguyen <[email protected]>\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"}
|
||||
@@ -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_ollama/modelchooser
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -17,9 +17,9 @@
|
||||
namespace aiprovider_ollama;
|
||||
|
||||
use core\http_client;
|
||||
use core_ai\aiactions\responses\response_base;
|
||||
use core_ai\process_base;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
@@ -38,14 +38,46 @@ abstract class abstract_processor extends process_base {
|
||||
*
|
||||
* @return UriInterface
|
||||
*/
|
||||
abstract protected function get_endpoint(): UriInterface;
|
||||
protected function get_endpoint(): UriInterface {
|
||||
$url = rtrim($this->provider->config['endpoint'], '/')
|
||||
. '/api/generate/';
|
||||
|
||||
return new Uri($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the model to use.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function get_model(): string;
|
||||
protected function get_model(): string {
|
||||
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['systeminstruction'],
|
||||
$settings['providerid'],
|
||||
$settings['modelextraparams'],
|
||||
);
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the system instructions.
|
||||
@@ -53,16 +85,17 @@ abstract class abstract_processor extends process_base {
|
||||
* @return string
|
||||
*/
|
||||
protected function get_system_instruction(): string {
|
||||
return $this->action->get_system_instruction();
|
||||
return $this->action::get_system_instruction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the request object to send to the Ollama API.
|
||||
* This object contains all the required parameters for the request.
|
||||
*
|
||||
* @param string $userid The user id.
|
||||
* @return RequestInterface The request object to send to the Ollama API.
|
||||
*/
|
||||
abstract protected function create_request_object(): RequestInterface;
|
||||
abstract protected function create_request_object(string $userid): RequestInterface;
|
||||
|
||||
/**
|
||||
* Handle a successful response from the external AI api.
|
||||
@@ -74,7 +107,7 @@ abstract class abstract_processor extends process_base {
|
||||
|
||||
#[\Override]
|
||||
protected function query_ai_api(): array {
|
||||
$request = $this->create_request_object();
|
||||
$request = $this->create_request_object($this->provider->generate_userid($this->action->get_configuration('userid')));
|
||||
$request = $this->provider->add_authentication_headers($request);
|
||||
|
||||
$client = \core\di::get(http_client::class);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?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_ollama\aimodel;
|
||||
|
||||
use core_ai\aimodel\base;
|
||||
use MoodleQuickForm;
|
||||
|
||||
/**
|
||||
* Llama 3.3 AI model.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class llama33 extends base implements ollama_base {
|
||||
|
||||
#[\Override]
|
||||
public function get_model_name(): string {
|
||||
return 'llama3.3';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function get_model_display_name(): string {
|
||||
return 'Llama 3.3';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function has_model_settings(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function add_model_settings(MoodleQuickForm $mform): void {
|
||||
$mform->addElement(
|
||||
'text',
|
||||
'mirostat',
|
||||
get_string('settings_mirostat', 'aiprovider_ollama'),
|
||||
);
|
||||
$mform->setType('mirostat', PARAM_INT);
|
||||
$mform->addHelpButton('mirostat', 'settings_mirostat', 'aiprovider_ollama');
|
||||
|
||||
$mform->addElement(
|
||||
'text',
|
||||
'temperature',
|
||||
get_string('settings_temperature', 'aiprovider_ollama'),
|
||||
);
|
||||
$mform->setType('temperature', PARAM_FLOAT);
|
||||
$mform->addHelpButton('temperature', 'settings_temperature', 'aiprovider_ollama');
|
||||
|
||||
$mform->addElement(
|
||||
'text',
|
||||
'seed',
|
||||
get_string('settings_seed', 'aiprovider_ollama'),
|
||||
);
|
||||
$mform->setType('seed', PARAM_INT);
|
||||
$mform->addHelpButton('seed', 'settings_seed', 'aiprovider_ollama');
|
||||
|
||||
$mform->addElement(
|
||||
'text',
|
||||
'top_k',
|
||||
get_string('settings_top_k', 'aiprovider_ollama'),
|
||||
);
|
||||
$mform->setType('top_k', PARAM_FLOAT);
|
||||
$mform->addHelpButton('top_k', 'settings_top_k', 'aiprovider_ollama');
|
||||
|
||||
$mform->addElement(
|
||||
'text',
|
||||
'top_p',
|
||||
get_string('settings_top_p', 'aiprovider_ollama'),
|
||||
);
|
||||
$mform->setType('top_p', PARAM_FLOAT);
|
||||
$mform->addHelpButton('top_p', 'settings_top_p', 'aiprovider_ollama');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function model_type(): int {
|
||||
return self::MODEL_TYPE_TEXT;
|
||||
}
|
||||
}
|
||||
@@ -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_ollama\aimodel;
|
||||
|
||||
/**
|
||||
* Ollama base AI model interface.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
interface ollama_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;
|
||||
}
|
||||
@@ -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_ollama\form;
|
||||
|
||||
use aiprovider_ollama\helper;
|
||||
use core_ai\form\action_settings_form;
|
||||
|
||||
/**
|
||||
* Base action settings form for Ollama provider.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @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_ollama';
|
||||
|
||||
$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_ollama');
|
||||
}
|
||||
}
|
||||
|
||||
// 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_ollama/modelchooser', 'init');
|
||||
$mform = $this->_form;
|
||||
|
||||
// Action model to use.
|
||||
$mform->addElement(
|
||||
'select',
|
||||
'modeltemplate',
|
||||
get_string("action:{$this->actionname}:model", 'aiprovider_ollama'),
|
||||
$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'] ?? 'llama3.3';
|
||||
}
|
||||
$mform->setDefault('modeltemplate', $defaultmodel);
|
||||
$mform->addHelpButton('modeltemplate', "action:{$this->actionname}:model", 'aiprovider_ollama');
|
||||
|
||||
$mform->addElement('hidden', 'model', $this->actionconfig['model'] ?? 'llama3.3');
|
||||
$mform->setType('model', PARAM_TEXT);
|
||||
|
||||
$mform->addElement('text', 'custommodel', get_string('custom_model_name', 'aiprovider_ollama'));
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -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_ollama\form;
|
||||
|
||||
use aiprovider_ollama\aimodel\ollama_base;
|
||||
|
||||
/**
|
||||
* Base action settings form for Ollama provider.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class action_generate_text_form extends action_form {
|
||||
#[\Override]
|
||||
protected function definition(): void {
|
||||
parent::definition();
|
||||
$mform = $this->_form;
|
||||
|
||||
$this->add_model_fields(ollama_base::MODEL_TYPE_TEXT);
|
||||
|
||||
// System Instructions.
|
||||
$mform->addElement(
|
||||
'textarea',
|
||||
'systeminstruction',
|
||||
get_string("action:{$this->actionname}:systeminstruction", 'aiprovider_ollama'),
|
||||
'wrap="virtual" rows="5" cols="20"',
|
||||
);
|
||||
$mform->setType('systeminstruction', PARAM_TEXT);
|
||||
$mform->setDefault('systeminstruction', $actionconfig['systeminstruction'] ?? $this->action::get_system_instruction());
|
||||
$mform->addHelpButton('systeminstruction', "action:{$this->actionname}:systeminstruction", 'aiprovider_ollama');
|
||||
|
||||
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', $this->action);
|
||||
$mform->setType('action', PARAM_TEXT);
|
||||
|
||||
// Add the provider class as a hidden field.
|
||||
$mform->addElement('hidden', 'provider', $this->providername);
|
||||
$mform->setType('provider', PARAM_TEXT);
|
||||
|
||||
// Add the provider id as a hidden field.
|
||||
$mform->addElement('hidden', 'providerid', $this->providerid);
|
||||
$mform->setType('providerid', PARAM_INT);
|
||||
|
||||
$this->set_data($this->actionconfig);
|
||||
}
|
||||
}
|
||||
@@ -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_ollama;
|
||||
|
||||
use core_ai\aimodel\base;
|
||||
|
||||
/**
|
||||
* Helper class for the Ollama provider.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @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_ollama', '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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?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_ollama;
|
||||
|
||||
use core_ai\hook\after_ai_action_settings_form_hook;
|
||||
use core_ai\hook\after_ai_provider_form_hook;
|
||||
|
||||
/**
|
||||
* Hook listener for Ollama Provider.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class hook_listener {
|
||||
|
||||
/**
|
||||
* Hook listener for the Ollama instance setup form.
|
||||
*
|
||||
* @param after_ai_provider_form_hook $hook The hook to add to the AI instance setup.
|
||||
*/
|
||||
public static function set_form_definition_for_aiprovider_ollama(after_ai_provider_form_hook $hook): void {
|
||||
if ($hook->plugin !== 'aiprovider_ollama') {
|
||||
return;
|
||||
}
|
||||
|
||||
$mform = $hook->mform;
|
||||
|
||||
// Setting to store Ollama endpoint URL.
|
||||
$mform->addElement(
|
||||
'text',
|
||||
'endpoint',
|
||||
get_string('endpoint', 'aiprovider_ollama'),
|
||||
['size' => 25],
|
||||
);
|
||||
$mform->setType('endpoint', PARAM_URL);
|
||||
$mform->addHelpButton('endpoint', 'endpoint', 'aiprovider_ollama');
|
||||
$mform->addRule('endpoint', get_string('required'), 'required', null, 'client');
|
||||
$mform->setDefault('endpoint', 'http://localhost:11434');
|
||||
|
||||
// Checkbox to enable basic auth settings.
|
||||
$mform->addElement(
|
||||
'checkbox',
|
||||
'enablebasicauth',
|
||||
get_string('enablebasicauth', 'aiprovider_ollama')
|
||||
);
|
||||
$mform->setType('enablebasicauth', PARAM_INT);
|
||||
$mform->addHelpButton('enablebasicauth', 'enablebasicauth', 'aiprovider_ollama');
|
||||
$mform->setDefault('enablebasicauth', 0);
|
||||
|
||||
// Username for basic auth.
|
||||
$mform->addElement(
|
||||
'text',
|
||||
'username',
|
||||
get_string('username', 'aiprovider_ollama'),
|
||||
);
|
||||
$mform->setType('username', PARAM_TEXT);
|
||||
$mform->addHelpButton('username', 'username', 'aiprovider_ollama');
|
||||
$mform->hideIf('username', 'enablebasicauth', 'notchecked');
|
||||
|
||||
// Password for basic auth.
|
||||
// Username for basic auth.
|
||||
$mform->addElement(
|
||||
'passwordunmask',
|
||||
'password',
|
||||
get_string('password', 'aiprovider_ollama'),
|
||||
);
|
||||
$mform->setType('password', PARAM_TEXT);
|
||||
$mform->addHelpButton('password', 'password', 'aiprovider_ollama');
|
||||
$mform->hideIf('password', 'enablebasicauth', 'notchecked');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook listener for the Ollama 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_ollama(after_ai_action_settings_form_hook $hook): void {
|
||||
if ($hook->plugin !== 'aiprovider_ollama') {
|
||||
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_ollama'));
|
||||
$mform->addElement('html', get_string('settings_help', 'aiprovider_ollama'));
|
||||
$mform->addElement(
|
||||
'textarea',
|
||||
'modelextraparams',
|
||||
get_string('extraparams', 'aiprovider_ollama'),
|
||||
['rows' => 5, 'cols' => 20],
|
||||
);
|
||||
$mform->setType('modelextraparams', PARAM_TEXT);
|
||||
$mform->addElement('static', 'modelextraparams_help', null, get_string('extraparams_help', 'aiprovider_ollama'));
|
||||
} else {
|
||||
$targetmodel = helper::get_model_class($model);
|
||||
if ($targetmodel) {
|
||||
if ($targetmodel->has_model_settings()) {
|
||||
$mform->addElement('header', 'modelsettingsheader', get_string('settings', 'aiprovider_ollama'));
|
||||
$mform->addElement('html', get_string('settings_help', 'aiprovider_ollama'));
|
||||
$targetmodel->add_model_settings($mform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,7 @@ use core_privacy\local\request\userlist;
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider
|
||||
{
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
#[\Override]
|
||||
public static function get_metadata(collection $collection): collection {
|
||||
$collection->add_external_location_link('aiprovider_ollama', [
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
namespace aiprovider_ollama;
|
||||
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Class process text generation.
|
||||
@@ -30,30 +28,20 @@ use Psr\Http\Message\UriInterface;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class process_generate_text extends abstract_processor {
|
||||
#[\Override]
|
||||
protected function get_endpoint(): UriInterface {
|
||||
$url = rtrim(get_config('aiprovider_ollama', 'endpoint'), '/')
|
||||
. '/api/generate';
|
||||
return new Uri($url);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function get_model(): string {
|
||||
return get_config('aiprovider_ollama', 'action_generate_text_model');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function get_system_instruction(): string {
|
||||
return get_config('aiprovider_ollama', 'action_generate_text_systeminstruction');
|
||||
return $this->provider->actionconfig[$this->action::class]['settings']['systeminstruction'];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function create_request_object(): RequestInterface {
|
||||
protected function create_request_object(string $userid): RequestInterface {
|
||||
// Create the request object.
|
||||
$requestobj = new \stdClass();
|
||||
$requestobj->model = $this->get_model();
|
||||
$requestobj->stream = false;
|
||||
$requestobj->prompt = $this->action->get_configuration('prompttext');
|
||||
$requestobj->user = $userid;
|
||||
$requestobj->options = new \stdClass();
|
||||
|
||||
// If there is a system string available, use it.
|
||||
$systeminstruction = $this->get_system_instruction();
|
||||
@@ -61,6 +49,12 @@ class process_generate_text extends abstract_processor {
|
||||
$requestobj->system = $systeminstruction;
|
||||
}
|
||||
|
||||
// Append the extra model settings.
|
||||
$modelsettings = $this->get_model_settings();
|
||||
foreach ($modelsettings as $setting => $value) {
|
||||
$requestobj->options->$setting = $value;
|
||||
}
|
||||
|
||||
return new Request(
|
||||
method: 'POST',
|
||||
uri: '',
|
||||
@@ -87,6 +81,7 @@ class process_generate_text extends abstract_processor {
|
||||
'finishreason' => $bodyobj->done_reason,
|
||||
'prompttokens' => $bodyobj->prompt_eval_count,
|
||||
'completiontokens' => $bodyobj->eval_count,
|
||||
'model' => $bodyobj->model ?? $this->get_model(), // Fallback to config model.
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
namespace aiprovider_ollama;
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Class process text summarisation.
|
||||
*
|
||||
@@ -27,13 +24,8 @@ use Psr\Http\Message\UriInterface;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class process_summarise_text extends process_generate_text {
|
||||
#[\Override]
|
||||
protected function get_model(): string {
|
||||
return get_config('aiprovider_ollama', 'action_summarise_text_model');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function get_system_instruction(): string {
|
||||
return get_config('aiprovider_ollama', 'action_summarise_text_systeminstruction');
|
||||
return $this->provider->actionconfig[$this->action::class]['settings']['systeminstruction'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
namespace aiprovider_ollama;
|
||||
|
||||
use core_ai\aiactions;
|
||||
use core_ai\rate_limiter;
|
||||
use core_ai\form\action_settings_form;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
@@ -28,153 +27,60 @@ use Psr\Http\Message\RequestInterface;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider extends \core_ai\provider {
|
||||
/** @var bool Is basic authentication enabled. */
|
||||
private bool $basicauthenabled;
|
||||
|
||||
/** @var string The basic auth username. */
|
||||
private string $username;
|
||||
|
||||
/** @var string The basic auth password. */
|
||||
private string $password;
|
||||
|
||||
/** @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() {
|
||||
// Basic auth enabled.
|
||||
$this->basicauthenabled = get_config('aiprovider_ollama', 'enablebasicauth');
|
||||
// Get basic auth username from config.
|
||||
$this->username = get_config('aiprovider_ollama', 'username');
|
||||
// Get basic auth password from config.
|
||||
$this->password = get_config('aiprovider_ollama', 'password');
|
||||
// Get global rate limit from config.
|
||||
$this->enableglobalratelimit = get_config('aiprovider_ollama', 'enableglobalratelimit');
|
||||
$this->globalratelimit = get_config('aiprovider_ollama', 'globalratelimit');
|
||||
// Get user rate limit from config.
|
||||
$this->enableuserratelimit = get_config('aiprovider_ollama', 'enableuserratelimit');
|
||||
$this->userratelimit = get_config('aiprovider_ollama', 'userratelimit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of actions that this provider supports.
|
||||
*
|
||||
* @return array An array of action class names.
|
||||
*/
|
||||
public function get_action_list(): array {
|
||||
#[\Override]
|
||||
public static function get_action_list(): array {
|
||||
return [
|
||||
\core_ai\aiactions\generate_text::class,
|
||||
\core_ai\aiactions\summarise_text::class,
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function get_action_settings(
|
||||
string $action,
|
||||
array $customdata = [],
|
||||
): action_settings_form|bool {
|
||||
$actionname = substr($action, (strrpos($action, '\\') + 1));
|
||||
$customdata['actionname'] = $actionname;
|
||||
$customdata['action'] = $action;
|
||||
$customdata['providername'] = 'aiprovider_ollama';
|
||||
if ($actionname === 'generate_text' || $actionname === 'summarise_text') {
|
||||
return new form\action_generate_text_form(customdata: $customdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a request to add any headers required by the provider.
|
||||
*
|
||||
* @param \Psr\Http\Message\RequestInterface $request
|
||||
* @return \Psr\Http\Message\RequestInterface
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function add_authentication_headers(RequestInterface $request): RequestInterface {
|
||||
if (!$this->basicauthenabled) {
|
||||
if (empty($this->config['basicauthenabled'])) {
|
||||
return $request;
|
||||
} else {
|
||||
// Add the Authorization header for basic auth
|
||||
$authHeader = 'Basic ' . base64_encode($this->username . ':' . $this->password);
|
||||
return $request
|
||||
->withAddedHeader('Authorization', $authHeader);
|
||||
// Add the Authorization header for basic auth.
|
||||
$authheader = 'Basic ' . base64_encode($this->config['username'] . ':' . $this->config['password']);
|
||||
return $request->withAddedHeader('Authorization', $authheader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request is allowed by the rate limiter.
|
||||
*
|
||||
* @param aiactions\base $action The action to check.
|
||||
* @return array|bool True on success, array of error details on failure.
|
||||
*/
|
||||
public function is_request_allowed(aiactions\base $action): array|bool {
|
||||
$ratelimiter = \core\di::get(rate_limiter::class);
|
||||
$component = \core\component::get_component_from_classname(get_class($this));
|
||||
|
||||
// Check the user rate limit.
|
||||
if ($this->enableuserratelimit) {
|
||||
if (!$ratelimiter->check_user_rate_limit(
|
||||
component: $component,
|
||||
ratelimit: $this->userratelimit,
|
||||
userid: $action->get_configuration('userid')
|
||||
)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'errorcode' => 429,
|
||||
'errormessage' => 'User rate limit exceeded',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Check the global rate limit.
|
||||
if ($this->enableglobalratelimit) {
|
||||
if (!$ratelimiter->check_global_rate_limit(
|
||||
component: $component,
|
||||
ratelimit: $this->globalratelimit
|
||||
)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'errorcode' => 429,
|
||||
'errormessage' => 'Global rate limit exceeded',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any action settings for this provider.
|
||||
*
|
||||
* @param string $action The action class name.
|
||||
* @param \admin_root $ADMIN The admin root object.
|
||||
* @param string $section The section name.
|
||||
* @param bool $hassiteconfig Whether the current user has moodle/site:config capability.
|
||||
* @return array An array of settings.
|
||||
*/
|
||||
public function get_action_settings(
|
||||
string $action,
|
||||
\admin_root $ADMIN,
|
||||
string $section,
|
||||
bool $hassiteconfig
|
||||
): array {
|
||||
$actionname = substr($action, (strrpos($action, '\\') + 1));
|
||||
$settings = [];
|
||||
#[\Override]
|
||||
public static function get_action_setting_defaults(string $action): array {
|
||||
$actionname = substr($action, (strrpos($action, '\\') + 1));
|
||||
$customdata = [
|
||||
'actionname' => $actionname,
|
||||
'action' => $action,
|
||||
'providername' => 'aiprovider_ollama',
|
||||
];
|
||||
if ($actionname === 'generate_text' || $actionname === 'summarise_text') {
|
||||
// Add the model setting.
|
||||
$settings[] = new \admin_setting_configtext(
|
||||
"aiprovider_ollama/action_{$actionname}_model",
|
||||
new \lang_string("action:{$actionname}:model", 'aiprovider_ollama'),
|
||||
new \lang_string("action:{$actionname}:model_desc", 'aiprovider_ollama'),
|
||||
'llama3.1:8b',
|
||||
PARAM_TEXT,
|
||||
);
|
||||
// Add system instruction settings.
|
||||
$settings[] = new \admin_setting_configtextarea(
|
||||
"aiprovider_ollama/action_{$actionname}_systeminstruction",
|
||||
new \lang_string("action:{$actionname}:systeminstruction", 'aiprovider_ollama'),
|
||||
new \lang_string("action:{$actionname}:systeminstruction_desc", 'aiprovider_ollama'),
|
||||
$action::get_system_instruction(),
|
||||
PARAM_TEXT
|
||||
);
|
||||
$mform = new form\action_generate_text_form(customdata: $customdata);
|
||||
return $mform->get_defaults();
|
||||
}
|
||||
|
||||
return $settings;
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function is_provider_configured(): bool {
|
||||
return !empty($this->config['endpoint']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Hook listener callbacks for the Ollama Provider.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => \core_ai\hook\after_ai_provider_form_hook::class,
|
||||
'callback' => \aiprovider_ollama\hook_listener::class . '::set_form_definition_for_aiprovider_ollama',
|
||||
],
|
||||
[
|
||||
'hook' => \core_ai\hook\after_ai_action_settings_form_hook::class,
|
||||
'callback' => \aiprovider_ollama\hook_listener::class . '::set_model_form_definition_for_aiprovider_ollama',
|
||||
],
|
||||
];
|
||||
@@ -22,34 +22,46 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['action:generate_text:endpoint'] = 'API endpoint';
|
||||
$string['action:generate_text:endpoint_desc'] = 'The API endpoint for the provider uses for this action.';
|
||||
$string['action:generate_text:model'] = 'Text generation model';
|
||||
$string['action:generate_text:model_desc'] = 'The model used to generate the text response.';
|
||||
$string['action:generate_text:model_help'] = 'The model used to generate the text response.';
|
||||
$string['action:generate_text:systeminstruction'] = 'System instruction';
|
||||
$string['action:generate_text:systeminstruction_desc'] = 'This instruction is provided together with the user prompt for this action. It provides information to the AI model on how to generate the response.';
|
||||
$string['action:generate_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.';
|
||||
$string['action:summarise_text:model'] = 'Text summarisation model';
|
||||
$string['action:summarise_text:model_desc'] = 'The model used to summarise the provided text.';
|
||||
$string['action:summarise_text:model_help'] = 'The model used to summarise the provided text.';
|
||||
$string['action:summarise_text:systeminstruction'] = 'System instruction';
|
||||
$string['action:summarise_text:systeminstruction_desc'] = 'This instruction is provided together with the user prompt for this action. It provides information to the AI model on how to generate the response.';
|
||||
$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['custom_model_name'] = 'Custom model name';
|
||||
$string['enablebasicauth'] = 'Enable basic authentication';
|
||||
$string['enablebasicauth_desc'] = 'Enable basic authentication for the Ollama API provider.';
|
||||
$string['enableglobalratelimit'] = 'Enable global rate limiting';
|
||||
$string['enableglobalratelimit_desc'] = 'Enable global rate limiting for the Ollama API provider.';
|
||||
$string['enableuserratelimit'] = 'Enable user rate limiting';
|
||||
$string['enableuserratelimit_desc'] = 'Enable user rate limiting for the Ollama API provider.';
|
||||
$string['enablebasicauth_help'] = 'Enable basic authentication for the Ollama API provider.';
|
||||
$string['endpoint'] = 'API endpoint';
|
||||
$string['endpoint_desc'] = 'The API endpoint for the Ollama API server.';
|
||||
$string['globalratelimit'] = 'Global rate limit';
|
||||
$string['globalratelimit_desc'] = 'Set the number of requests per hour allowed for the global rate limit.';
|
||||
$string['endpoint_help'] = 'The API endpoint for the Ollama API server.';
|
||||
$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['password'] = 'Password';
|
||||
$string['password_desc'] = 'The password used for basic authentication.';
|
||||
$string['password_help'] = 'The password used for basic authentication.';
|
||||
$string['pluginname'] = 'Ollama API Provider';
|
||||
$string['privacy:metadata'] = 'The Ollama API provider plugin does not store any personal data.';
|
||||
$string['privacy:metadata:aiprovider_ollama:externalpurpose'] = 'This information is sent to the Ollama API in order for a response to be generated. Your Ollama account settings may change how Ollama stores and retains this data. No user data is explicitly sent to Ollama or stored in Moodle LMS by this plugin.';
|
||||
$string['privacy:metadata:aiprovider_ollama:model'] = 'The model used to generate the response.';
|
||||
$string['privacy:metadata:aiprovider_ollama:prompttext'] = 'The user entered text prompt used to generate the response.';
|
||||
$string['settings'] = 'Settings';
|
||||
$string['settings_help'] = 'You can adjust the settings below to customize how requests are sent to Ollama. Update the values as needed, ensuring they align with your requirements.<br><br>';
|
||||
$string['settings_mirostat'] = 'mirostat';
|
||||
$string['settings_mirostat_help'] = 'Enable Mirostat sampling for controlling perplexity. (default: 0, 0 = disabled, 1 = Mirostat, 2 = Mirostat 2.0)';
|
||||
$string['settings_seed'] = 'seed';
|
||||
$string['settings_seed_help'] = 'Sets the random number seed to use for generation. Setting this to a specific number will make the model generate the same text for the same prompt. (Default: 0)';
|
||||
$string['settings_temperature'] = 'temperature';
|
||||
$string['settings_temperature_help'] = 'The temperature of the model. Increasing the temperature will make the model answer more creatively. (Default: 0.8)';
|
||||
$string['settings_top_k'] = 'top_k';
|
||||
$string['settings_top_k_help'] = 'Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)';
|
||||
$string['settings_top_p'] = 'top_p';
|
||||
$string['settings_top_p_help'] = 'Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)';
|
||||
$string['username'] = 'Username';
|
||||
$string['username_desc'] = 'The username used for basic authentication.';
|
||||
$string['userratelimit'] = 'User rate limit';
|
||||
$string['userratelimit_desc'] = 'Set the number of requests per hour allowed for the user rate limit.';
|
||||
$string['username_help'] = 'The username used for basic authentication.';
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<?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_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @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_ollama',
|
||||
new lang_string('pluginname', 'aiprovider_ollama'), 'moodle/site:config', true);
|
||||
|
||||
$settings->add(new admin_setting_heading('aiprovider_ollama/general',
|
||||
new lang_string('providersettings', 'core_ai'),
|
||||
new lang_string('providersettings_desc', 'core_ai')));
|
||||
|
||||
// Setting to store Ollama API URL endpoint.
|
||||
$settings->add(new admin_setting_configtext('aiprovider_ollama/endpoint',
|
||||
new lang_string('endpoint', 'aiprovider_ollama'),
|
||||
new lang_string('endpoint_desc', 'aiprovider_ollama'),
|
||||
'http://localhost:11434',
|
||||
PARAM_URL));
|
||||
|
||||
// Checkbox to enable basic auth settings.
|
||||
$settings->add(new admin_setting_configcheckbox('aiprovider_ollama/enablebasicauth',
|
||||
new lang_string('enablebasicauth', 'aiprovider_ollama'),
|
||||
new lang_string('enablebasicauth_desc', 'aiprovider_ollama'),
|
||||
0));
|
||||
|
||||
// Username for basic auth.
|
||||
$settings->add(new admin_setting_configtext('aiprovider_ollama/username',
|
||||
new lang_string('username', 'aiprovider_ollama'),
|
||||
new lang_string('username_desc', 'aiprovider_ollama'),
|
||||
'',
|
||||
PARAM_TEXT));
|
||||
$settings->hide_if('aiprovider_ollama/username', 'aiprovider_ollama/enablebasicauth', 'eq', 0);
|
||||
|
||||
// Password for basic auth.
|
||||
$settings->add(new admin_setting_configpasswordunmask('aiprovider_ollama/password',
|
||||
new lang_string('password', 'aiprovider_ollama'),
|
||||
new lang_string('password_desc', 'aiprovider_ollama'),
|
||||
''
|
||||
));
|
||||
$settings->hide_if('aiprovider_ollama/password', 'aiprovider_ollama/enablebasicauth', 'eq', 0);
|
||||
|
||||
// Setting to enable/disable global rate limiting.
|
||||
$settings->add(new admin_setting_configcheckbox('aiprovider_ollama/enableglobalratelimit',
|
||||
new lang_string('enableglobalratelimit', 'aiprovider_ollama'),
|
||||
new lang_string('enableglobalratelimit_desc', 'aiprovider_ollama'),
|
||||
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_ollama/globalratelimit',
|
||||
new lang_string('globalratelimit', 'aiprovider_ollama'),
|
||||
new lang_string('globalratelimit_desc', 'aiprovider_ollama'),
|
||||
100,
|
||||
PARAM_INT));
|
||||
$settings->hide_if('aiprovider_ollama/globalratelimit', 'aiprovider_ollama/enableglobalratelimit', 'eq', 0);
|
||||
|
||||
// Setting to enable/disable user rate limiting.
|
||||
$settings->add(new admin_setting_configcheckbox('aiprovider_ollama/enableuserratelimit',
|
||||
new lang_string('enableuserratelimit', 'aiprovider_ollama'),
|
||||
new lang_string('enableuserratelimit_desc', 'aiprovider_ollama'),
|
||||
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_ollama/userratelimit',
|
||||
new lang_string('userratelimit', 'aiprovider_ollama'),
|
||||
new lang_string('userratelimit_desc', 'aiprovider_ollama'),
|
||||
10,
|
||||
PARAM_INT));
|
||||
$settings->hide_if('aiprovider_ollama/userratelimit', 'aiprovider_ollama/enableuserratelimit', 'eq', 0);
|
||||
}
|
||||
@@ -16,11 +16,12 @@
|
||||
|
||||
namespace aiprovider_ollama;
|
||||
|
||||
use aiprovider_ollama\process_generate_text;
|
||||
use core_ai\aiactions\base;
|
||||
use core_ai\provider;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
require_once(__DIR__ . '/testcase_helper_trait.php');
|
||||
|
||||
/**
|
||||
* Test Generate text provider class for Ollama provider methods.
|
||||
*
|
||||
@@ -32,9 +33,15 @@ use GuzzleHttp\Psr7\Response;
|
||||
* @covers \aiprovider_ollama\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;
|
||||
|
||||
/** @var \core_ai\manager AI Manager. */
|
||||
private $manager;
|
||||
|
||||
/** @var provider The provider that will process the action. */
|
||||
protected provider $provider;
|
||||
|
||||
@@ -46,19 +53,19 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->resetAfterTest();
|
||||
// Load a response body from a file.
|
||||
$this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_ollama', 'text_request_success.json'));
|
||||
$this->create_provider();
|
||||
$this->manager = \core\di::get(\core_ai\manager::class);
|
||||
$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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the provider object.
|
||||
*/
|
||||
private function create_provider(): void {
|
||||
$this->provider = new \aiprovider_ollama\provider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the action object.
|
||||
* @param int $userid The user id to use in the action.
|
||||
@@ -84,7 +91,55 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
$body = (object) json_decode($request->getBody()->getContents());
|
||||
|
||||
$this->assertEquals('This is a test prompt', $body->prompt);
|
||||
$this->assertEquals('llama3.1:8b', $body->model);
|
||||
$this->assertEquals('llama3.2', $body->model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
'mirostat' => '1',
|
||||
'seed' => '50',
|
||||
],
|
||||
);
|
||||
$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('llama3.2', $body->model);
|
||||
$this->assertEquals('0.5', $body->options->temperature);
|
||||
$this->assertEquals('1', $body->options->mirostat);
|
||||
$this->assertEquals('50', $body->options->seed);
|
||||
|
||||
$this->provider = $this->create_provider(
|
||||
actionclass: \core_ai\aiactions\generate_text::class,
|
||||
actionconfig: [
|
||||
'model' => 'my-custom-ollama',
|
||||
'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
|
||||
'modelextraparams' => '{"temperature": 0.5,"mirostat": 1,"seed": "50"}',
|
||||
],
|
||||
);
|
||||
$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-ollama', $body->model);
|
||||
$this->assertEquals('0.5', $body->options->temperature);
|
||||
$this->assertEquals('1', $body->options->mirostat);
|
||||
$this->assertEquals('50', $body->options->seed);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,14 +351,30 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
$clock = $this->mock_clock_with_frozen();
|
||||
|
||||
// Set the user rate limiter.
|
||||
set_config('enableuserratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('userratelimit', 1, 'aiprovider_ollama');
|
||||
$config = [
|
||||
'enableuserratelimit' => true,
|
||||
'userratelimit' => 1,
|
||||
'endpoint' => "http://localhost:11434/",
|
||||
];
|
||||
|
||||
$provider = $this->manager->create_provider_instance(
|
||||
classname: '\aiprovider_ollama\provider',
|
||||
name: 'dummy',
|
||||
config: $config,
|
||||
actionconfig: [
|
||||
\core_ai\aiactions\generate_text::class => [
|
||||
'settings' => [
|
||||
'model' => 'llama3.2',
|
||||
'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// Case 1: User rate limit has not been reached.
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
@@ -311,7 +382,7 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$processor = new process_generate_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
@@ -323,9 +394,8 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$processor = new process_generate_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertEquals(429, $result->get_errorcode());
|
||||
$this->assertEquals('User rate limit exceeded', $result->get_errormessage());
|
||||
@@ -334,7 +404,6 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
// Case 3: User rate limit has not been reached for a different user.
|
||||
// Log in user2.
|
||||
$this->setUser($user2);
|
||||
$this->create_provider();
|
||||
$this->create_action($user2->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
@@ -342,7 +411,7 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$processor = new process_generate_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
@@ -356,9 +425,8 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$processor = new process_generate_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
}
|
||||
@@ -377,14 +445,30 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
$clock = $this->mock_clock_with_frozen();
|
||||
|
||||
// Set the global rate limiter.
|
||||
set_config('enableglobalratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('globalratelimit', 1, 'aiprovider_ollama');
|
||||
$config = [
|
||||
'enableglobalratelimit' => true,
|
||||
'globalratelimit' => 1,
|
||||
'endpoint' => "http://localhost:11434/",
|
||||
];
|
||||
|
||||
$provider = $this->manager->create_provider_instance(
|
||||
classname: '\aiprovider_ollama\provider',
|
||||
name: 'dummy',
|
||||
config: $config,
|
||||
actionconfig: [
|
||||
\core_ai\aiactions\generate_text::class => [
|
||||
'settings' => [
|
||||
'model' => 'llama3.2',
|
||||
'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// Case 1: Global rate limit has not been reached.
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
@@ -392,7 +476,7 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$processor = new process_generate_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
@@ -404,9 +488,8 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$processor = new process_generate_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertEquals(429, $result->get_errorcode());
|
||||
$this->assertEquals('Global rate limit exceeded', $result->get_errormessage());
|
||||
@@ -415,7 +498,6 @@ final class process_generate_text_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->create_action($user2->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
@@ -423,7 +505,7 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$processor = new process_generate_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertFalse($result->get_success());
|
||||
|
||||
@@ -437,9 +519,8 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$processor = new process_generate_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
|
||||
namespace aiprovider_ollama;
|
||||
|
||||
use aiprovider_ollama\process_summarise_text;
|
||||
use core_ai\aiactions\base;
|
||||
use core_ai\provider;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
require_once(__DIR__ . '/testcase_helper_trait.php');
|
||||
|
||||
/**
|
||||
* Test Generate text provider class for Ollama provider methods.
|
||||
*
|
||||
@@ -32,9 +33,15 @@ use GuzzleHttp\Psr7\Response;
|
||||
* @covers \aiprovider_ollama\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;
|
||||
|
||||
/** @var \core_ai\manager AI Manager. */
|
||||
private $manager;
|
||||
|
||||
/** @var provider The provider that will process the action. */
|
||||
protected provider $provider;
|
||||
|
||||
@@ -46,19 +53,19 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->resetAfterTest();
|
||||
// Load a response body from a file.
|
||||
$this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_ollama', 'text_request_success.json'));
|
||||
$this->create_provider();
|
||||
$this->manager = \core\di::get(\core_ai\manager::class);
|
||||
$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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the provider object.
|
||||
*/
|
||||
private function create_provider(): void {
|
||||
$this->provider = new \aiprovider_ollama\provider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the action object.
|
||||
* @param int $userid The user id to use in the action.
|
||||
@@ -84,7 +91,55 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
$body = (object) json_decode($request->getBody()->getContents());
|
||||
|
||||
$this->assertEquals('This is a test prompt', $body->prompt);
|
||||
$this->assertEquals('llama3.1:8b', $body->model);
|
||||
$this->assertEquals('llama3.2', $body->model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
'mirostat' => '1',
|
||||
'seed' => '50',
|
||||
],
|
||||
);
|
||||
$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('llama3.2', $body->model);
|
||||
$this->assertEquals('0.5', $body->options->temperature);
|
||||
$this->assertEquals('1', $body->options->mirostat);
|
||||
$this->assertEquals('50', $body->options->seed);
|
||||
|
||||
$this->provider = $this->create_provider(
|
||||
actionclass: \core_ai\aiactions\summarise_text::class,
|
||||
actionconfig: [
|
||||
'model' => 'my-custom-ollama',
|
||||
'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
|
||||
'modelextraparams' => '{"temperature": 0.5,"mirostat": 1,"seed": "50"}',
|
||||
],
|
||||
);
|
||||
$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-ollama', $body->model);
|
||||
$this->assertEquals('0.5', $body->options->temperature);
|
||||
$this->assertEquals('1', $body->options->mirostat);
|
||||
$this->assertEquals('50', $body->options->seed);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,14 +342,30 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
$clock = $this->mock_clock_with_frozen();
|
||||
|
||||
// Set the user rate limiter.
|
||||
set_config('enableuserratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('userratelimit', 1, 'aiprovider_ollama');
|
||||
$config = [
|
||||
'enableuserratelimit' => true,
|
||||
'userratelimit' => 1,
|
||||
'endpoint' => "http://localhost:11434/",
|
||||
];
|
||||
|
||||
$provider = $this->manager->create_provider_instance(
|
||||
classname: '\aiprovider_ollama\provider',
|
||||
name: 'dummy',
|
||||
config: $config,
|
||||
actionconfig: [
|
||||
\core_ai\aiactions\summarise_text::class => [
|
||||
'settings' => [
|
||||
'model' => 'llama3.2',
|
||||
'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// Case 1: User rate limit has not been reached.
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
@@ -302,7 +373,7 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$processor = new process_summarise_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
@@ -314,9 +385,8 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$processor = new process_summarise_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertEquals(429, $result->get_errorcode());
|
||||
$this->assertEquals('User rate limit exceeded', $result->get_errormessage());
|
||||
@@ -325,7 +395,6 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
// Case 3: User rate limit has not been reached for a different user.
|
||||
// Log in user2.
|
||||
$this->setUser($user2);
|
||||
$this->create_provider();
|
||||
$this->create_action($user2->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
@@ -333,7 +402,7 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$processor = new process_summarise_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
@@ -347,9 +416,8 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$processor = new process_summarise_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
}
|
||||
@@ -368,14 +436,30 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
$clock = $this->mock_clock_with_frozen();
|
||||
|
||||
// Set the global rate limiter.
|
||||
set_config('enableglobalratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('globalratelimit', 1, 'aiprovider_ollama');
|
||||
$config = [
|
||||
'enableglobalratelimit' => true,
|
||||
'globalratelimit' => 1,
|
||||
'endpoint' => "http://localhost:11434/",
|
||||
];
|
||||
|
||||
$provider = $this->manager->create_provider_instance(
|
||||
classname: '\aiprovider_ollama\provider',
|
||||
name: 'dummy',
|
||||
config: $config,
|
||||
actionconfig: [
|
||||
\core_ai\aiactions\summarise_text::class => [
|
||||
'settings' => [
|
||||
'model' => 'llama3.2',
|
||||
'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// Case 1: Global rate limit has not been reached.
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
@@ -383,7 +467,7 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$processor = new process_summarise_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
@@ -395,9 +479,8 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$processor = new process_summarise_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertEquals(429, $result->get_errorcode());
|
||||
$this->assertEquals('Global rate limit exceeded', $result->get_errormessage());
|
||||
@@ -406,7 +489,6 @@ final class process_summarise_text_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->create_action($user2->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
@@ -414,7 +496,7 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$processor = new process_summarise_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertFalse($result->get_success());
|
||||
|
||||
@@ -428,9 +510,8 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$processor = new process_summarise_text($provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
}
|
||||
|
||||
@@ -25,12 +25,35 @@ namespace aiprovider_ollama;
|
||||
* @covers \aiprovider_ollama\provider
|
||||
*/
|
||||
final class provider_test extends \advanced_testcase {
|
||||
|
||||
/** @var \core_ai\manager */
|
||||
private $manager;
|
||||
|
||||
/** @var \core_ai\provider */
|
||||
private $provider;
|
||||
|
||||
/**
|
||||
* Overriding setUp() function to always reset after tests.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create the provider instance.
|
||||
$this->manager = \core\di::get(\core_ai\manager::class);
|
||||
$config = ['data' => 'goeshere'];
|
||||
$this->provider = $this->manager->create_provider_instance(
|
||||
classname: '\aiprovider_ollama\provider',
|
||||
name: 'dummy',
|
||||
config: $config,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_action_list
|
||||
*/
|
||||
public function test_get_action_list(): void {
|
||||
$provider = new provider();
|
||||
$actionlist = $provider->get_action_list();
|
||||
$actionlist = $this->provider->get_action_list();
|
||||
$this->assertIsArray($actionlist);
|
||||
$this->assertCount(2, $actionlist);
|
||||
$this->assertContains(\core_ai\aiactions\generate_text::class, $actionlist);
|
||||
@@ -41,31 +64,28 @@ final class provider_test extends \advanced_testcase {
|
||||
* Test is_request_allowed.
|
||||
*/
|
||||
public function test_is_request_allowed(): void {
|
||||
$this->resetAfterTest();
|
||||
// Create the provider instance.
|
||||
$config = [
|
||||
'enableuserratelimit' => true,
|
||||
'userratelimit' => 3,
|
||||
'enableglobalratelimit' => true,
|
||||
'globalratelimit' => 5,
|
||||
];
|
||||
|
||||
// Set plugin config rate limiter settings.
|
||||
set_config('enableglobalratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('globalratelimit', 5, 'aiprovider_ollama');
|
||||
set_config('enableuserratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('userratelimit', 3, 'aiprovider_ollama');
|
||||
$provider = $this->manager->create_provider_instance(
|
||||
classname: '\aiprovider_ollama\provider',
|
||||
name: 'dummy',
|
||||
config: $config,
|
||||
);
|
||||
|
||||
$contextid = 1;
|
||||
$userid = 1;
|
||||
$prompttext = 'This is a test prompt';
|
||||
$aspectratio = 'square';
|
||||
$quality = 'hd';
|
||||
$numimages = 1;
|
||||
$style = 'vivid';
|
||||
$action = new \core_ai\aiactions\generate_image(
|
||||
$action = new \core_ai\aiactions\generate_text(
|
||||
contextid: $contextid,
|
||||
userid: $userid,
|
||||
prompttext: $prompttext,
|
||||
quality: $quality,
|
||||
aspectratio: $aspectratio,
|
||||
numimages: $numimages,
|
||||
style: $style,
|
||||
);
|
||||
$provider = new provider();
|
||||
|
||||
// Make 3 requests, all should be allowed.
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
@@ -78,14 +98,10 @@ final class provider_test extends \advanced_testcase {
|
||||
$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(
|
||||
$action = new \core_ai\aiactions\generate_text(
|
||||
contextid: $contextid,
|
||||
userid: 2,
|
||||
prompttext: $prompttext,
|
||||
quality: $quality,
|
||||
aspectratio: $aspectratio,
|
||||
numimages: $numimages,
|
||||
style: $style,
|
||||
);
|
||||
$this->assertTrue($provider->is_request_allowed($action));
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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_ollama;
|
||||
|
||||
/**
|
||||
* Trait for test cases.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2025 Huong Nguyen <[email protected]>
|
||||
* @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 = [
|
||||
'enableuserratelimit' => true,
|
||||
'userratelimit' => 1,
|
||||
'enableglobalratelimit' => true,
|
||||
'globalratelimit' => 1,
|
||||
'endpoint' => "http://localhost:11434/",
|
||||
];
|
||||
$defaultactionconfig = [
|
||||
$actionclass => [
|
||||
'settings' => [
|
||||
'model' => 'llama3.2',
|
||||
],
|
||||
],
|
||||
];
|
||||
foreach ($actionconfig as $key => $value) {
|
||||
$defaultactionconfig[$actionclass]['settings'][$key] = $value;
|
||||
}
|
||||
$provider = $manager->create_provider_instance(
|
||||
classname: '\aiprovider_ollama\provider',
|
||||
name: 'dummy',
|
||||
config: $config,
|
||||
actionconfig: $defaultactionconfig,
|
||||
);
|
||||
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
@@ -41,27 +41,7 @@ class provider extends \core_ai\provider {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a request to add any headers required by the provider.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return RequestInterface
|
||||
*/
|
||||
#[\Override]
|
||||
public function add_authentication_headers(RequestInterface $request): RequestInterface {
|
||||
if (isset($this->config['orgid'])) {
|
||||
return $request
|
||||
|
||||
@@ -130,3 +130,18 @@ Feature: An administrator can manage AI subsystem settings
|
||||
And I click on the "Settings" link in the table row containing "Text editor placement"
|
||||
And I should see "This action is unavailable." in the table row containing "Generate text"
|
||||
Then I should not see "This action is unavailable." in the table row containing "Generate image"
|
||||
|
||||
@javascript
|
||||
Scenario: An administrator can control the enabled state of AI placement actions using JavaScript
|
||||
Given the following "core_ai > ai providers" exist:
|
||||
| provider | name | enabled | endpoint |
|
||||
| aiprovider_ollama | Ollama API test | 1 | http://localhost:11434 |
|
||||
And I am logged in as "admin"
|
||||
And I navigate to "AI > AI placements" in site administration
|
||||
When I click on the "Settings" link in the table row containing "Text editor placement"
|
||||
Then I should see "Generate text" in the "flexible" "table"
|
||||
And I should see "Generate image" in the "flexible" "table"
|
||||
And I should not see "This action is unavailable. No AI providers are configured for this action." in the "Generate text" "table_row"
|
||||
And I should see "This action is unavailable. No AI providers are configured for this action." in the "Generate image" "table_row"
|
||||
And I toggle the "Generate text" admin switch "off"
|
||||
And I should see "Generate text disabled."
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
"allowedlevel2": true,
|
||||
"allowedspread": false
|
||||
},
|
||||
"aimodel": {
|
||||
"component": "core_ai",
|
||||
"allowedlevel2": true,
|
||||
"allowedspread": false
|
||||
},
|
||||
"analytics": {
|
||||
"component": "core_analytics",
|
||||
"allowedlevel2": true,
|
||||
|
||||
Reference in New Issue
Block a user