MDL-83006 AI: Provider Plugin - Ollama
Ollama is an open-source project that serves as a powerful and user-friendly platform for running LLMs on your local machine. It acts as a bridge between the complexities of LLM technology and the desire for an accessible and customizable AI experience. This plugin interfaces with the upstream AI service and will make available AI Actions to the subsystem.
This commit is contained in:
committed by
Huong Nguyen
parent
89d76fdc25
commit
90227e4707
@@ -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\http_client;
|
||||
use core_ai\aiactions\responses\response_base;
|
||||
use core_ai\process_base;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Class process text generation.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class abstract_processor extends process_base {
|
||||
/**
|
||||
* Get the endpoint URI.
|
||||
*
|
||||
* @return UriInterface
|
||||
*/
|
||||
abstract protected function get_endpoint(): UriInterface;
|
||||
|
||||
/**
|
||||
* Get the name of the model to use.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function get_model(): string;
|
||||
|
||||
/**
|
||||
* Get the system instructions.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_system_instruction(): string {
|
||||
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.
|
||||
*
|
||||
* @return RequestInterface The request object to send to the Ollama API.
|
||||
*/
|
||||
abstract protected function create_request_object(): RequestInterface;
|
||||
|
||||
/**
|
||||
* Handle a successful response from the external AI api.
|
||||
*
|
||||
* @param ResponseInterface $response The response object.
|
||||
* @return array The response.
|
||||
*/
|
||||
abstract protected function handle_api_success(ResponseInterface $response): array;
|
||||
|
||||
#[\Override]
|
||||
protected function query_ai_api(): array {
|
||||
$request = $this->create_request_object();
|
||||
$request = $this->provider->add_authentication_headers($request);
|
||||
|
||||
$client = \core\di::get(http_client::class);
|
||||
try {
|
||||
// Call the external AI service.
|
||||
$response = $client->send($request, [
|
||||
'base_uri' => $this->get_endpoint(),
|
||||
RequestOptions::HTTP_ERRORS => false,
|
||||
]);
|
||||
} catch (RequestException $e) {
|
||||
// Handle any exceptions.
|
||||
return [
|
||||
'success' => false,
|
||||
'errorcode' => $e->getCode(),
|
||||
'errormessage' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
// Double-check the response codes, in case of a non 200 that didn't throw an error.
|
||||
$status = $response->getStatusCode();
|
||||
if ($status === 200) {
|
||||
return $this->handle_api_success($response);
|
||||
} else {
|
||||
return $this->handle_api_error($response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an error from the external AI api.
|
||||
*
|
||||
* @param ResponseInterface $response The response object.
|
||||
* @return array The error response.
|
||||
*/
|
||||
protected function handle_api_error(ResponseInterface $response): array {
|
||||
$responsearr = [
|
||||
'success' => false,
|
||||
'errorcode' => $response->getStatusCode(),
|
||||
];
|
||||
|
||||
$status = $response->getStatusCode();
|
||||
if ($status >= 500 && $status < 600) {
|
||||
$responsearr['errormessage'] = $response->getReasonPhrase();
|
||||
} else {
|
||||
$bodyobj = json_decode($response->getBody()->getContents());
|
||||
$responsearr['errormessage'] = $bodyobj->error->message;
|
||||
}
|
||||
|
||||
return $responsearr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?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\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
/**
|
||||
* Privacy provider implementation for Ollama provider
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider
|
||||
{
|
||||
#[\Override]
|
||||
public static function get_metadata(collection $collection): collection {
|
||||
$collection->add_external_location_link('aiprovider_ollama', [
|
||||
'prompttext' => 'privacy:metadata:aiprovider_ollama:prompttext',
|
||||
'model' => 'privacy:metadata:aiprovider_ollama:model',
|
||||
], 'privacy:metadata:aiprovider_ollama:externalpurpose');
|
||||
return $collection;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function get_contexts_for_userid(int $userid): contextlist {
|
||||
return new contextlist();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function export_user_data(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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 GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Class process text generation.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @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');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function create_request_object(): RequestInterface {
|
||||
// Create the request object.
|
||||
$requestobj = new \stdClass();
|
||||
$requestobj->model = $this->get_model();
|
||||
$requestobj->stream = false;
|
||||
$requestobj->prompt = $this->action->get_configuration('prompttext');
|
||||
|
||||
// If there is a system string available, use it.
|
||||
$systeminstruction = $this->get_system_instruction();
|
||||
if (!empty($systeminstruction)) {
|
||||
$requestobj->system = $systeminstruction;
|
||||
}
|
||||
|
||||
return new Request(
|
||||
method: 'POST',
|
||||
uri: '',
|
||||
body: json_encode($requestobj),
|
||||
headers: [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a successful response from the external AI api.
|
||||
*
|
||||
* @param ResponseInterface $response The response object.
|
||||
* @return array The response.
|
||||
*/
|
||||
protected function handle_api_success(ResponseInterface $response): array {
|
||||
$responsebody = $response->getBody();
|
||||
$bodyobj = json_decode($responsebody->getContents());
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'generatedcontent' => $bodyobj->response,
|
||||
'finishreason' => $bodyobj->done_reason,
|
||||
'prompttokens' => $bodyobj->prompt_eval_count,
|
||||
'completiontokens' => $bodyobj->eval_count,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Class process text summarisation.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?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\aiactions;
|
||||
use core_ai\rate_limiter;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Class provider.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @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 {
|
||||
return [
|
||||
\core_ai\aiactions\generate_text::class,
|
||||
\core_ai\aiactions\summarise_text::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update a request to add any headers required by the provider.
|
||||
*
|
||||
* @param \Psr\Http\Message\RequestInterface $request
|
||||
* @return \Psr\Http\Message\RequestInterface
|
||||
*/
|
||||
public function add_authentication_headers(RequestInterface $request): RequestInterface {
|
||||
if (!$this->basicauthenabled) {
|
||||
return $request;
|
||||
} else {
|
||||
// Add the Authorization header for basic auth
|
||||
$authHeader = 'Basic ' . base64_encode($this->username . ':' . $this->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 = [];
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Strings for component aiprovider_ollama, language 'en'.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @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: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:summarise_text:model'] = 'Text summarisation model';
|
||||
$string['action:summarise_text:model_desc'] = '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['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['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['password'] = 'Password';
|
||||
$string['password_desc'] = '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['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.';
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Plugin administration pages are defined here.
|
||||
*
|
||||
* @package aiprovider_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);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
{
|
||||
"model":"mistral",
|
||||
"created_at":"2024-09-09T01:54:37.479368Z",
|
||||
"response":" World War II was a global conflict that lasted from 1939 to 1945. It involved the majority of the world's countries—organized into two opposing military alliances: the Allies and the Axis powers. The main spark for the war was Adolf Hitler's invasion of Poland, which led to declarations of war on Germany by Britain and France. The war concluded with the unconditional surrender of the Axis powers. It resulted in 70-85 million fatalities, making it one of the deadliest conflicts in human history.",
|
||||
"done":true,
|
||||
"done_reason":"stop",
|
||||
"context":[
|
||||
3,
|
||||
29473,
|
||||
1763,
|
||||
1390,
|
||||
6324,
|
||||
1032,
|
||||
3013,
|
||||
3555,
|
||||
1245,
|
||||
1040,
|
||||
2956,
|
||||
29491,
|
||||
4372,
|
||||
4406,
|
||||
1117,
|
||||
1066,
|
||||
9038,
|
||||
3013,
|
||||
3586,
|
||||
1124,
|
||||
1420,
|
||||
2927,
|
||||
29491,
|
||||
10590,
|
||||
1935,
|
||||
3046,
|
||||
12150,
|
||||
8998,
|
||||
781,
|
||||
1028,
|
||||
29508,
|
||||
29491,
|
||||
5339,
|
||||
1040,
|
||||
14828,
|
||||
1065,
|
||||
11603,
|
||||
3013,
|
||||
1633,
|
||||
6256,
|
||||
781,
|
||||
1028,
|
||||
29518,
|
||||
29491,
|
||||
3146,
|
||||
1227,
|
||||
3792,
|
||||
1475,
|
||||
2484,
|
||||
4022,
|
||||
1989,
|
||||
15526,
|
||||
29493,
|
||||
4424,
|
||||
1067,
|
||||
1510,
|
||||
29493,
|
||||
1210,
|
||||
25242,
|
||||
16555,
|
||||
29491,
|
||||
781,
|
||||
781,
|
||||
6006,
|
||||
29473,
|
||||
29550,
|
||||
29502,
|
||||
3853,
|
||||
1124,
|
||||
1043,
|
||||
29495,
|
||||
29518,
|
||||
4,
|
||||
1027,
|
||||
4072,
|
||||
4041,
|
||||
4485,
|
||||
1171,
|
||||
1032,
|
||||
4294,
|
||||
11086,
|
||||
1137,
|
||||
24121,
|
||||
1245,
|
||||
29473,
|
||||
29508,
|
||||
29542,
|
||||
29538,
|
||||
29542,
|
||||
1066,
|
||||
29473,
|
||||
29508,
|
||||
29542,
|
||||
29549,
|
||||
29550,
|
||||
29491,
|
||||
1429,
|
||||
6058,
|
||||
1040,
|
||||
8525,
|
||||
1070,
|
||||
1040,
|
||||
2294,
|
||||
29510,
|
||||
29481,
|
||||
6548,
|
||||
29589,
|
||||
9742,
|
||||
2100,
|
||||
1546,
|
||||
1757,
|
||||
6561,
|
||||
1056,
|
||||
6237,
|
||||
1312,
|
||||
21672,
|
||||
29515,
|
||||
1040,
|
||||
2450,
|
||||
1265,
|
||||
1072,
|
||||
1040,
|
||||
1098,
|
||||
13426,
|
||||
12518,
|
||||
29491,
|
||||
1183,
|
||||
2959,
|
||||
14891,
|
||||
1122,
|
||||
1040,
|
||||
2264,
|
||||
1171,
|
||||
2732,
|
||||
5977,
|
||||
15624,
|
||||
29510,
|
||||
29481,
|
||||
19561,
|
||||
1070,
|
||||
18580,
|
||||
29493,
|
||||
1458,
|
||||
4931,
|
||||
1066,
|
||||
18995,
|
||||
1465,
|
||||
1070,
|
||||
2264,
|
||||
1124,
|
||||
8061,
|
||||
1254,
|
||||
10942,
|
||||
1072,
|
||||
5611,
|
||||
29491,
|
||||
1183,
|
||||
2264,
|
||||
17257,
|
||||
1163,
|
||||
1040,
|
||||
1289,
|
||||
1981,
|
||||
3350,
|
||||
26652,
|
||||
1070,
|
||||
1040,
|
||||
1098,
|
||||
13426,
|
||||
12518,
|
||||
29491,
|
||||
1429,
|
||||
17728,
|
||||
1065,
|
||||
29473,
|
||||
29555,
|
||||
29502,
|
||||
29501,
|
||||
29551,
|
||||
29550,
|
||||
4609,
|
||||
20942,
|
||||
1986,
|
||||
29493,
|
||||
3260,
|
||||
1146,
|
||||
1392,
|
||||
1070,
|
||||
1040,
|
||||
4462,
|
||||
18835,
|
||||
24897,
|
||||
1065,
|
||||
3698,
|
||||
4108,
|
||||
29491
|
||||
],
|
||||
"total_duration":63199305763,
|
||||
"load_duration":28276495,
|
||||
"prompt_eval_count":75,
|
||||
"prompt_eval_duration":6918955000,
|
||||
"eval_count":123,
|
||||
"eval_duration":56249258000
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
<?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 aiprovider_ollama\process_generate_text;
|
||||
use core_ai\aiactions\base;
|
||||
use core_ai\provider;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
/**
|
||||
* Test Generate text provider class for Ollama provider methods.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \aiprovider_ollama\provider
|
||||
* @covers \aiprovider_ollama\process_generate_text
|
||||
* @covers \aiprovider_ollama\abstract_processor
|
||||
*/
|
||||
final class process_generate_text_test extends \advanced_testcase {
|
||||
/** @var string A successful response in JSON format. */
|
||||
protected string $responsebodyjson;
|
||||
|
||||
/** @var provider The provider that will process the action. */
|
||||
protected provider $provider;
|
||||
|
||||
/** @var base The action to process. */
|
||||
protected base $action;
|
||||
|
||||
/**
|
||||
* Set up the test.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Load a response body from a file.
|
||||
$this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_ollama', 'text_request_success.json'));
|
||||
$this->create_provider();
|
||||
$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.
|
||||
*/
|
||||
private function create_action(int $userid = 1): void {
|
||||
$this->action = new \core_ai\aiactions\generate_text(
|
||||
contextid: 1,
|
||||
userid: $userid,
|
||||
prompttext: 'This is a test prompt',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create_request_object
|
||||
*/
|
||||
public function test_create_request_object(): void {
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
|
||||
// We're working with a private method here, so we need to use reflection.
|
||||
$method = new \ReflectionMethod($processor, 'create_request_object');
|
||||
$request = $method->invoke($processor, 1);
|
||||
|
||||
$body = (object) json_decode($request->getBody()->getContents());
|
||||
|
||||
$this->assertEquals('This is a test prompt', $body->prompt);
|
||||
$this->assertEquals('llama3.1:8b', $body->model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the API error response handler method.
|
||||
*/
|
||||
public function test_handle_api_error(): void {
|
||||
$responses = [
|
||||
500 => new Response(500, ['Content-Type' => 'application/json']),
|
||||
503 => new Response(503, ['Content-Type' => 'application/json']),
|
||||
401 => new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Invalid Authentication']]),
|
||||
),
|
||||
404 => new Response(
|
||||
404,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'You must be a member of an organization to use the API']]),
|
||||
),
|
||||
429 => new Response(
|
||||
429,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Rate limit reached for requests']]),
|
||||
),
|
||||
];
|
||||
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$method = new \ReflectionMethod($processor, 'handle_api_error');
|
||||
|
||||
foreach ($responses as $status => $response) {
|
||||
$result = $method->invoke($processor, $response);
|
||||
$this->assertEquals($status, $result['errorcode']);
|
||||
if ($status == 500) {
|
||||
$this->assertEquals('Internal Server Error', $result['errormessage']);
|
||||
} else if ($status == 503) {
|
||||
$this->assertEquals('Service Unavailable', $result['errormessage']);
|
||||
} else {
|
||||
$this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the API success response handler method.
|
||||
*/
|
||||
public function test_handle_api_success(): void {
|
||||
$response = new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
);
|
||||
|
||||
// We're testing a private method, so we need to setup reflector magic.
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$method = new \ReflectionMethod($processor, 'handle_api_success');
|
||||
|
||||
$result = $method->invoke($processor, $response);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('World War II was a global conflict', $result['generatedcontent']);
|
||||
$this->assertEquals('stop', $result['finishreason']);
|
||||
$this->assertEquals('75', $result['prompttokens']);
|
||||
$this->assertEquals('123', $result['completiontokens']);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test query_ai_api for a successful call.
|
||||
*/
|
||||
public function test_query_ai_api_success(): void {
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$method = new \ReflectionMethod($processor, 'query_ai_api');
|
||||
$result = $method->invoke($processor);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('World War II was a global conflict', $result['generatedcontent']);
|
||||
$this->assertEquals('stop', $result['finishreason']);
|
||||
$this->assertEquals('75', $result['prompttokens']);
|
||||
$this->assertEquals('123', $result['completiontokens']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test prepare_response success.
|
||||
*/
|
||||
public function test_prepare_response_success(): void {
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
|
||||
// We're working with a private method here, so we need to use reflection.
|
||||
$method = new \ReflectionMethod($processor, 'prepare_response');
|
||||
|
||||
$response = [
|
||||
'success' => true,
|
||||
'id' => 'chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo',
|
||||
'fingerprint' => 'fp_c4e5b6fa31',
|
||||
'generatedcontent' => 'Sure, here is some sample text',
|
||||
'finishreason' => 'stop',
|
||||
'prompttokens' => '11',
|
||||
'completiontokens' => '568',
|
||||
];
|
||||
|
||||
$result = $method->invoke($processor, $response);
|
||||
|
||||
$this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
|
||||
$this->assertTrue($result->get_success());
|
||||
$this->assertEquals('generate_text', $result->get_actionname());
|
||||
$this->assertEquals($response['success'], $result->get_success());
|
||||
$this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test prepare_response error.
|
||||
*/
|
||||
public function test_prepare_response_error(): void {
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
|
||||
// We're working with a private method here, so we need to use reflection.
|
||||
$method = new \ReflectionMethod($processor, 'prepare_response');
|
||||
|
||||
$response = [
|
||||
'success' => false,
|
||||
'errorcode' => 500,
|
||||
'errormessage' => 'Internal server error.',
|
||||
];
|
||||
|
||||
$result = $method->invoke($processor, $response);
|
||||
|
||||
$this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
|
||||
$this->assertFalse($result->get_success());
|
||||
$this->assertEquals('generate_text', $result->get_actionname());
|
||||
$this->assertEquals($response['errorcode'], $result->get_errorcode());
|
||||
$this->assertEquals($response['errormessage'], $result->get_errormessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test process method.
|
||||
*/
|
||||
public function test_process(): void {
|
||||
$this->resetAfterTest();
|
||||
// Log in user.
|
||||
$this->setUser($this->getDataGenerator()->create_user());
|
||||
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
|
||||
$this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
|
||||
$this->assertTrue($result->get_success());
|
||||
$this->assertEquals('generate_text', $result->get_actionname());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test process method with error.
|
||||
*/
|
||||
public function test_process_error(): void {
|
||||
$this->resetAfterTest();
|
||||
// Log in user.
|
||||
$this->setUser($this->getDataGenerator()->create_user());
|
||||
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Invalid Authentication']]),
|
||||
));
|
||||
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
|
||||
$this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
|
||||
$this->assertFalse($result->get_success());
|
||||
$this->assertEquals('generate_text', $result->get_actionname());
|
||||
$this->assertEquals(401, $result->get_errorcode());
|
||||
$this->assertEquals('Invalid Authentication', $result->get_errormessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test process method with user rate limiter.
|
||||
*/
|
||||
public function test_process_with_user_rate_limiter(): void {
|
||||
$this->resetAfterTest();
|
||||
// Create users.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
// Log in user1.
|
||||
$this->setUser($user1);
|
||||
// Mock clock.
|
||||
$clock = $this->mock_clock_with_frozen();
|
||||
|
||||
// Set the user rate limiter.
|
||||
set_config('enableuserratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('userratelimit', 1, 'aiprovider_ollama');
|
||||
|
||||
// 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(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
// Case 2: User rate limit has been reached.
|
||||
$clock->bump(HOURSECS - 10);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertEquals(429, $result->get_errorcode());
|
||||
$this->assertEquals('User rate limit exceeded', $result->get_errormessage());
|
||||
$this->assertFalse($result->get_success());
|
||||
|
||||
// Case 3: User rate limit has not been reached for a different user.
|
||||
// Log in user2.
|
||||
$this->setUser($user2);
|
||||
$this->create_provider();
|
||||
$this->create_action($user2->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
// Case 4: Time window has passed, user rate limit should be reset.
|
||||
$clock->bump(11);
|
||||
// Log in user1.
|
||||
$this->setUser($user1);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test process method with global rate limiter.
|
||||
*/
|
||||
public function test_process_with_global_rate_limiter(): void {
|
||||
$this->resetAfterTest();
|
||||
// Create users.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
// Log in user1.
|
||||
$this->setUser($user1);
|
||||
// Mock clock.
|
||||
$clock = $this->mock_clock_with_frozen();
|
||||
|
||||
// Set the global rate limiter.
|
||||
set_config('enableglobalratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('globalratelimit', 1, 'aiprovider_ollama');
|
||||
|
||||
// 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(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
// Case 2: Global rate limit has been reached.
|
||||
$clock->bump(HOURSECS - 10);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertEquals(429, $result->get_errorcode());
|
||||
$this->assertEquals('Global rate limit exceeded', $result->get_errormessage());
|
||||
$this->assertFalse($result->get_success());
|
||||
|
||||
// Case 3: Global rate limit has been reached for a different user too.
|
||||
// Log in user2.
|
||||
$this->setUser($user2);
|
||||
$this->create_provider();
|
||||
$this->create_action($user2->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertFalse($result->get_success());
|
||||
|
||||
// Case 4: Time window has passed, global rate limit should be reset.
|
||||
$clock->bump(11);
|
||||
// Log in user1.
|
||||
$this->setUser($user1);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
<?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 aiprovider_ollama\process_summarise_text;
|
||||
use core_ai\aiactions\base;
|
||||
use core_ai\provider;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
/**
|
||||
* Test Generate text provider class for Ollama provider methods.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \aiprovider_ollama\provider
|
||||
* @covers \aiprovider_ollama\process_summarise_text
|
||||
* @covers \aiprovider_ollama\abstract_processor
|
||||
*/
|
||||
final class process_summarise_text_test extends \advanced_testcase {
|
||||
/** @var string A successful response in JSON format. */
|
||||
protected string $responsebodyjson;
|
||||
|
||||
/** @var provider The provider that will process the action. */
|
||||
protected provider $provider;
|
||||
|
||||
/** @var base The action to process. */
|
||||
protected base $action;
|
||||
|
||||
/**
|
||||
* Set up the test.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Load a response body from a file.
|
||||
$this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_ollama', 'text_request_success.json'));
|
||||
$this->create_provider();
|
||||
$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.
|
||||
*/
|
||||
private function create_action(int $userid = 1): void {
|
||||
$this->action = new \core_ai\aiactions\summarise_text(
|
||||
contextid: 1,
|
||||
userid: $userid,
|
||||
prompttext: 'This is a test prompt',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create_request_object
|
||||
*/
|
||||
public function test_create_request_object(): void {
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
|
||||
// We're working with a private method here, so we need to use reflection.
|
||||
$method = new \ReflectionMethod($processor, 'create_request_object');
|
||||
$request = $method->invoke($processor, 1);
|
||||
|
||||
$body = (object) json_decode($request->getBody()->getContents());
|
||||
|
||||
$this->assertEquals('This is a test prompt', $body->prompt);
|
||||
$this->assertEquals('llama3.1:8b', $body->model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the API error response handler method.
|
||||
*
|
||||
*/
|
||||
public function test_handle_api_error(): void {
|
||||
$responses = [
|
||||
500 => new Response(500, ['Content-Type' => 'application/json']),
|
||||
503 => new Response(503, ['Content-Type' => 'application/json']),
|
||||
401 => new Response(401, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "Invalid Authentication"}}'),
|
||||
404 => new Response(404, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "You must be a member of an organization to use the API"}}'),
|
||||
429 => new Response(429, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "Rate limit reached for requests"}}'),
|
||||
];
|
||||
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$method = new \ReflectionMethod($processor, 'handle_api_error');
|
||||
|
||||
foreach ($responses as $status => $response) {
|
||||
$result = $method->invoke($processor, $response);
|
||||
$this->assertEquals($status, $result['errorcode']);
|
||||
if ($status == 500) {
|
||||
$this->assertEquals('Internal Server Error', $result['errormessage']);
|
||||
} else if ($status == 503) {
|
||||
$this->assertEquals('Service Unavailable', $result['errormessage']);
|
||||
} else {
|
||||
$this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the API success response handler method.
|
||||
*/
|
||||
public function test_handle_api_success(): void {
|
||||
$response = new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
);
|
||||
|
||||
// We're testing a private method, so we need to set up reflector magic.
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$method = new \ReflectionMethod($processor, 'handle_api_success');
|
||||
|
||||
$result = $method->invoke($processor, $response);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('World War II was a global conflict', $result['generatedcontent']);
|
||||
$this->assertEquals('stop', $result['finishreason']);
|
||||
$this->assertEquals('75', $result['prompttokens']);
|
||||
$this->assertEquals('123', $result['completiontokens']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test query_ai_api for a successful call.
|
||||
*/
|
||||
public function test_query_ai_api_success(): void {
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$method = new \ReflectionMethod($processor, 'query_ai_api');
|
||||
$result = $method->invoke($processor);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('World War II was a global conflict', $result['generatedcontent']);
|
||||
$this->assertEquals('stop', $result['finishreason']);
|
||||
$this->assertEquals('75', $result['prompttokens']);
|
||||
$this->assertEquals('123', $result['completiontokens']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test prepare_response success.
|
||||
*/
|
||||
public function test_prepare_response_success(): void {
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
|
||||
// We're working with a private method here, so we need to use reflection.
|
||||
$method = new \ReflectionMethod($processor, 'prepare_response');
|
||||
|
||||
$response = [
|
||||
'success' => true,
|
||||
'id' => 'chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo',
|
||||
'fingerprint' => 'fp_c4e5b6fa31',
|
||||
'generatedcontent' => 'Sure, here is some sample text',
|
||||
'finishreason' => 'stop',
|
||||
'prompttokens' => '11',
|
||||
'completiontokens' => '568',
|
||||
];
|
||||
|
||||
$result = $method->invoke($processor, $response);
|
||||
|
||||
$this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
|
||||
$this->assertTrue($result->get_success());
|
||||
$this->assertEquals('summarise_text', $result->get_actionname());
|
||||
$this->assertEquals($response['success'], $result->get_success());
|
||||
$this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test prepare_response error.
|
||||
*/
|
||||
public function test_prepare_response_error(): void {
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
|
||||
// We're working with a private method here, so we need to use reflection.
|
||||
$method = new \ReflectionMethod($processor, 'prepare_response');
|
||||
|
||||
$response = [
|
||||
'success' => false,
|
||||
'errorcode' => 500,
|
||||
'errormessage' => 'Internal server error.',
|
||||
];
|
||||
|
||||
$result = $method->invoke($processor, $response);
|
||||
|
||||
$this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
|
||||
$this->assertFalse($result->get_success());
|
||||
$this->assertEquals('summarise_text', $result->get_actionname());
|
||||
$this->assertEquals($response['errorcode'], $result->get_errorcode());
|
||||
$this->assertEquals($response['errormessage'], $result->get_errormessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test process method.
|
||||
*/
|
||||
public function test_process(): void {
|
||||
$this->resetAfterTest();
|
||||
// Log in user.
|
||||
$this->setUser($this->getDataGenerator()->create_user());
|
||||
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
|
||||
$this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
|
||||
$this->assertTrue($result->get_success());
|
||||
$this->assertEquals('summarise_text', $result->get_actionname());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test process method with error.
|
||||
*/
|
||||
public function test_process_error(): void {
|
||||
$this->resetAfterTest();
|
||||
// Log in user.
|
||||
$this->setUser($this->getDataGenerator()->create_user());
|
||||
|
||||
// Mock the http client to return a successful response.
|
||||
['mock' => $mock] = $this->get_mocked_http_client();
|
||||
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Invalid Authentication']]),
|
||||
));
|
||||
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
|
||||
$this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
|
||||
$this->assertFalse($result->get_success());
|
||||
$this->assertEquals('summarise_text', $result->get_actionname());
|
||||
$this->assertEquals(401, $result->get_errorcode());
|
||||
$this->assertEquals('Invalid Authentication', $result->get_errormessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test process method with user rate limiter.
|
||||
*/
|
||||
public function test_process_with_user_rate_limiter(): void {
|
||||
$this->resetAfterTest();
|
||||
// Create users.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
// Log in user1.
|
||||
$this->setUser($user1);
|
||||
// Mock clock.
|
||||
$clock = $this->mock_clock_with_frozen();
|
||||
|
||||
// Set the user rate limiter.
|
||||
set_config('enableuserratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('userratelimit', 1, 'aiprovider_ollama');
|
||||
|
||||
// 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(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
// Case 2: User rate limit has been reached.
|
||||
$clock->bump(HOURSECS - 10);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertEquals(429, $result->get_errorcode());
|
||||
$this->assertEquals('User rate limit exceeded', $result->get_errormessage());
|
||||
$this->assertFalse($result->get_success());
|
||||
|
||||
// Case 3: User rate limit has not been reached for a different user.
|
||||
// Log in user2.
|
||||
$this->setUser($user2);
|
||||
$this->create_provider();
|
||||
$this->create_action($user2->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
// Case 4: Time window has passed, user rate limit should be reset.
|
||||
$clock->bump(11);
|
||||
// Log in user1.
|
||||
$this->setUser($user1);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test process method with global rate limiter.
|
||||
*/
|
||||
public function test_process_with_global_rate_limiter(): void {
|
||||
$this->resetAfterTest();
|
||||
// Create users.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
// Log in user1.
|
||||
$this->setUser($user1);
|
||||
// Mock clock.
|
||||
$clock = $this->mock_clock_with_frozen();
|
||||
|
||||
// Set the global rate limiter.
|
||||
set_config('enableglobalratelimit', 1, 'aiprovider_ollama');
|
||||
set_config('globalratelimit', 1, 'aiprovider_ollama');
|
||||
|
||||
// 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(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
|
||||
// Case 2: Global rate limit has been reached.
|
||||
$clock->bump(HOURSECS - 10);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertEquals(429, $result->get_errorcode());
|
||||
$this->assertEquals('Global rate limit exceeded', $result->get_errormessage());
|
||||
$this->assertFalse($result->get_success());
|
||||
|
||||
// Case 3: Global rate limit has been reached for a different user too.
|
||||
// Log in user2.
|
||||
$this->setUser($user2);
|
||||
$this->create_provider();
|
||||
$this->create_action($user2->id);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertFalse($result->get_success());
|
||||
|
||||
// Case 4: Time window has passed, global rate limit should be reset.
|
||||
$clock->bump(11);
|
||||
// Log in user1.
|
||||
$this->setUser($user1);
|
||||
// The response from Ollama.
|
||||
$mock->append(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
$this->responsebodyjson,
|
||||
));
|
||||
$this->create_provider();
|
||||
$this->create_action($user1->id);
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
$result = $processor->process();
|
||||
$this->assertTrue($result->get_success());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Test Ollama provider methods.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \aiprovider_ollama\provider
|
||||
*/
|
||||
final class provider_test extends \advanced_testcase {
|
||||
/**
|
||||
* Test get_action_list
|
||||
*/
|
||||
public function test_get_action_list(): void {
|
||||
$provider = new provider();
|
||||
$actionlist = $provider->get_action_list();
|
||||
$this->assertIsArray($actionlist);
|
||||
$this->assertCount(2, $actionlist);
|
||||
$this->assertContains(\core_ai\aiactions\generate_text::class, $actionlist);
|
||||
$this->assertContains(\core_ai\aiactions\summarise_text::class, $actionlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is_request_allowed.
|
||||
*/
|
||||
public function test_is_request_allowed(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// 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');
|
||||
|
||||
$contextid = 1;
|
||||
$userid = 1;
|
||||
$prompttext = 'This is a test prompt';
|
||||
$aspectratio = 'square';
|
||||
$quality = 'hd';
|
||||
$numimages = 1;
|
||||
$style = 'vivid';
|
||||
$action = new \core_ai\aiactions\generate_image(
|
||||
contextid: $contextid,
|
||||
userid: $userid,
|
||||
prompttext: $prompttext,
|
||||
quality: $quality,
|
||||
aspectratio: $aspectratio,
|
||||
numimages: $numimages,
|
||||
style: $style,
|
||||
);
|
||||
$provider = new provider();
|
||||
|
||||
// Make 3 requests, all should be allowed.
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$this->assertTrue($provider->is_request_allowed($action));
|
||||
}
|
||||
|
||||
// The 4th request for the same user should be denied.
|
||||
$result = $provider->is_request_allowed($action);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertEquals('User rate limit exceeded', $result['errormessage']);
|
||||
|
||||
// Change user id to make a request for a different user, should pass (4 requests for global rate).
|
||||
$action = new \core_ai\aiactions\generate_image(
|
||||
contextid: $contextid,
|
||||
userid: 2,
|
||||
prompttext: $prompttext,
|
||||
quality: $quality,
|
||||
aspectratio: $aspectratio,
|
||||
numimages: $numimages,
|
||||
style: $style,
|
||||
);
|
||||
$this->assertTrue($provider->is_request_allowed($action));
|
||||
|
||||
// Make a 5th request for the global rate limit, it should be allowed.
|
||||
$this->assertTrue($provider->is_request_allowed($action));
|
||||
|
||||
// The 6th request should be denied.
|
||||
$result = $provider->is_request_allowed($action);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertEquals('Global rate limit exceeded', $result['errormessage']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Version information for aiprovider_ollama.
|
||||
*
|
||||
* @package aiprovider_ollama
|
||||
* @copyright 2024 Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'aiprovider_ollama';
|
||||
$plugin->version = 2024090600;
|
||||
$plugin->requires = 2024041600;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
Reference in New Issue
Block a user