diff --git a/README.md b/README.md index 79e9123..f3f76a2 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Currently supported AI tools: - OpenAI Dall-E (also via Azure) - Google Gemini - Google Synthesize (text to speech) +- Google Imagen 3 (via Vertex AI) - Ollama Currently available AI purposes: diff --git a/amd/build/vertexcachestatus.min.js b/amd/build/vertexcachestatus.min.js new file mode 100644 index 0000000..e4d3f9b --- /dev/null +++ b/amd/build/vertexcachestatus.min.js @@ -0,0 +1,11 @@ +define("local_ai_manager/vertexcachestatus",["exports","core/templates","core/ajax","core/notification","core/str"],(function(_exports,_templates,_ajax,_notification,_str){var obj; +/** + * Module rendering the warning box to inform the users about misleading AI results. + * + * @module local_ai_manager/vertexcachestatus + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};_exports.init=async selector=>{const statusElement=document.querySelector(selector),refreshButton=statusElement.querySelector('[data-action="refresh"]'),enableCachingButton=statusElement.querySelector('[data-action="enablecaching"]'),disableCachingButton=statusElement.querySelector('[data-action="disablecaching"]'),serviceaccountinfoTextArea=document.getElementById("id_serviceaccountjson");let serviceaccountinfo=serviceaccountinfoTextArea.value;serviceaccountinfoTextArea.addEventListener("input",(event=>{serviceaccountinfo=event.target.value})),refreshButton.addEventListener("click",(async event=>{event.preventDefault(),await updateCachingStatusDisplay(serviceaccountinfo,statusElement)})),enableCachingButton&&enableCachingButton.addEventListener("click",(async event=>{event.preventDefault(),enableCachingButton.disabled=!0,await updateCachingStatus(serviceaccountinfo,statusElement,!0)})),disableCachingButton&&disableCachingButton.addEventListener("click",(async event=>{event.preventDefault(),disableCachingButton.disabled=!0,await updateCachingStatus(serviceaccountinfo,statusElement,!1)}))};const updateCachingStatusDisplay=async(serviceaccountinfo,statusElement)=>{let queryResult=null;try{queryResult=await(serviceaccountinfo=>(0,_ajax.call)([{methodname:"local_ai_manager_vertex_cache_status",args:{serviceaccountinfo:serviceaccountinfo}}])[0])(serviceaccountinfo)}catch(error){return void await(0,_notification.exception)(error)}if(200!==queryResult.code){const errorTitleString=await(0,_str.getString)("vertex_error_cachestatus","local_ai_manager");await(0,_notification.alert)(errorTitleString,queryResult.error)}const templateContext={cachingEnabled:queryResult.cachingEnabled,noStatus:!1},{html:html,js:js}=await _templates.default.renderForPromise("local_ai_manager/vertexcachestatus",templateContext);_templates.default.replaceNode(statusElement,html,js)},updateCachingStatus=async(serviceaccountinfo,statusElement,newstatus)=>{let queryResult=null;try{queryResult=await((serviceaccountinfo,newstatus)=>(0,_ajax.call)([{methodname:"local_ai_manager_vertex_cache_status",args:{serviceaccountinfo:serviceaccountinfo,newstatus:newstatus}}])[0])(serviceaccountinfo,newstatus)}catch(error){return void await(0,_notification.exception)(error)}if(200===queryResult.code)await updateCachingStatusDisplay(serviceaccountinfo,statusElement);else{const errorTitleString=await(0,_str.getString)("vertex_error_cachestatus","local_ai_manager");await(0,_notification.alert)(errorTitleString,queryResult.error)}}})); + +//# sourceMappingURL=vertexcachestatus.min.js.map \ No newline at end of file diff --git a/amd/build/vertexcachestatus.min.js.map b/amd/build/vertexcachestatus.min.js.map new file mode 100644 index 0000000..24cea6f --- /dev/null +++ b/amd/build/vertexcachestatus.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"vertexcachestatus.min.js","sources":["../src/vertexcachestatus.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\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 .\n\n/**\n * Module rendering the warning box to inform the users about misleading AI results.\n *\n * @module local_ai_manager/vertexcachestatus\n * @copyright 2024 ISB Bayern\n * @author Philipp Memmel\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {call as fetchMany} from 'core/ajax';\nimport {alert as alertModal, exception as displayException} from 'core/notification';\nimport {getString} from 'core/str';\n\n/**\n * Fetches the current cache status of the specified service account.\n *\n * @param {string} serviceaccountinfo the stringified JSON with the service account info\n */\nconst fetchCurrentCacheStatus = (serviceaccountinfo) => fetchMany([{\n methodname: 'local_ai_manager_vertex_cache_status',\n args: {\n serviceaccountinfo\n }\n}])[0];\n\n/**\n * Updates the current cache status.\n *\n * @param {string} serviceaccountinfo the stringified JSON with the service account info\n * @param {boolean} newstatus true if the cache should be enabled, false if it should be disabled\n */\nconst setCurrentCacheStatus = (serviceaccountinfo, newstatus) => fetchMany([{\n methodname: 'local_ai_manager_vertex_cache_status',\n args: {\n serviceaccountinfo,\n newstatus\n }\n}])[0];\n\n/**\n * Controls and renders the Google Vertex AI cache status elements.\n *\n * @param {string} selector the CSS selector of the status element to operate on\n */\nexport const init = async(selector) => {\n const statusElement = document.querySelector(selector);\n const refreshButton = statusElement.querySelector('[data-action=\"refresh\"]');\n const enableCachingButton = statusElement.querySelector('[data-action=\"enablecaching\"]');\n const disableCachingButton = statusElement.querySelector('[data-action=\"disablecaching\"]');\n const serviceaccountinfoTextArea = document.getElementById('id_serviceaccountjson');\n let serviceaccountinfo = serviceaccountinfoTextArea.value;\n // We want to keep track of the current serviceaccountinfo data, also if the user changes it.\n serviceaccountinfoTextArea.addEventListener('input', (event) => {\n serviceaccountinfo = event.target.value;\n });\n\n refreshButton.addEventListener('click', async(event) => {\n event.preventDefault();\n await updateCachingStatusDisplay(serviceaccountinfo, statusElement);\n });\n\n if (enableCachingButton) {\n enableCachingButton.addEventListener('click', async(event) => {\n event.preventDefault();\n enableCachingButton.disabled = true;\n await updateCachingStatus(serviceaccountinfo, statusElement, true);\n });\n }\n if (disableCachingButton) {\n disableCachingButton.addEventListener('click', async(event) => {\n event.preventDefault();\n disableCachingButton.disabled = true;\n await updateCachingStatus(serviceaccountinfo, statusElement, false);\n });\n }\n};\n\n/**\n * Updates the caching status display.\n *\n * @param {string} serviceaccountinfo the stringified JSON with the service account info\n * @param {string} statusElement the HTML element to operate on\n */\nconst updateCachingStatusDisplay = async(serviceaccountinfo, statusElement) => {\n let queryResult = null;\n try {\n queryResult = await fetchCurrentCacheStatus(serviceaccountinfo);\n } catch (error) {\n await displayException(error);\n return;\n }\n if (queryResult.code !== 200) {\n const errorTitleString = await getString('vertex_error_cachestatus', 'local_ai_manager');\n await alertModal(errorTitleString, queryResult.error);\n }\n const templateContext = {\n cachingEnabled: queryResult.cachingEnabled,\n noStatus: false\n };\n\n const {html, js} = await Templates.renderForPromise('local_ai_manager/vertexcachestatus', templateContext);\n Templates.replaceNode(statusElement, html, js);\n};\n\n/**\n * Updates the caching status and updates the DOM to reflect the current state.\n *\n * @param {string} serviceaccountinfo the stringified JSON with the service account info\n * @param {string} statusElement the HTML element to operate on\n * @param {boolean} newstatus the status to set the caching configuration to (true or false)\n */\nconst updateCachingStatus = async(serviceaccountinfo, statusElement, newstatus) => {\n let queryResult = null;\n try {\n queryResult = await setCurrentCacheStatus(serviceaccountinfo, newstatus);\n } catch (error) {\n await displayException(error);\n return;\n }\n if (queryResult.code !== 200) {\n const errorTitleString = await getString('vertex_error_cachestatus', 'local_ai_manager');\n await alertModal(errorTitleString, queryResult.error);\n return;\n }\n await updateCachingStatusDisplay(serviceaccountinfo, statusElement);\n};\n"],"names":["async","statusElement","document","querySelector","selector","refreshButton","enableCachingButton","disableCachingButton","serviceaccountinfoTextArea","getElementById","serviceaccountinfo","value","addEventListener","event","target","preventDefault","updateCachingStatusDisplay","disabled","updateCachingStatus","queryResult","methodname","args","fetchCurrentCacheStatus","error","code","errorTitleString","templateContext","cachingEnabled","noStatus","html","js","Templates","renderForPromise","replaceNode","newstatus","setCurrentCacheStatus"],"mappings":";;;;;;;;8JA4DoBA,MAAAA,iBACVC,cAAgBC,SAASC,cAAcC,UACvCC,cAAgBJ,cAAcE,cAAc,2BAC5CG,oBAAsBL,cAAcE,cAAc,iCAClDI,qBAAuBN,cAAcE,cAAc,kCACnDK,2BAA6BN,SAASO,eAAe,6BACvDC,mBAAqBF,2BAA2BG,MAEpDH,2BAA2BI,iBAAiB,SAAUC,QAClDH,mBAAqBG,MAAMC,OAAOH,SAGtCN,cAAcO,iBAAiB,SAASZ,MAAAA,QACpCa,MAAME,uBACAC,2BAA2BN,mBAAoBT,kBAGrDK,qBACAA,oBAAoBM,iBAAiB,SAASZ,MAAAA,QAC1Ca,MAAME,iBACNT,oBAAoBW,UAAW,QACzBC,oBAAoBR,mBAAoBT,eAAe,MAGjEM,sBACAA,qBAAqBK,iBAAiB,SAASZ,MAAAA,QAC3Ca,MAAME,iBACNR,qBAAqBU,UAAW,QAC1BC,oBAAoBR,mBAAoBT,eAAe,aAWnEe,2BAA6BhB,MAAMU,mBAAoBT,qBACrDkB,YAAc,SAEdA,iBApEyBT,CAAAA,qBAAuB,cAAU,CAAC,CAC/DU,WAAY,uCACZC,KAAM,CACFX,mBAAAA,uBAEJ,GA+DwBY,CAAwBZ,oBAC9C,MAAOa,yBACC,2BAAiBA,UAGF,MAArBJ,YAAYK,KAAc,OACpBC,uBAAyB,kBAAU,2BAA4B,0BAC/D,uBAAWA,iBAAkBN,YAAYI,aAE7CG,gBAAkB,CACpBC,eAAgBR,YAAYQ,eAC5BC,UAAU,IAGRC,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiB,qCAAsCN,oCAChFO,YAAYhC,cAAe4B,KAAMC,KAUzCZ,oBAAsBlB,MAAMU,mBAAoBT,cAAeiC,iBAC7Df,YAAc,SAEdA,iBAnFsB,EAACT,mBAAoBwB,aAAc,cAAU,CAAC,CACxEd,WAAY,uCACZC,KAAM,CACFX,mBAAAA,mBACAwB,UAAAA,cAEJ,GA6EwBC,CAAsBzB,mBAAoBwB,WAChE,MAAOX,yBACC,2BAAiBA,UAGF,MAArBJ,YAAYK,WAKVR,2BAA2BN,mBAAoBT,0BAJ3CwB,uBAAyB,kBAAU,2BAA4B,0BAC/D,uBAAWA,iBAAkBN,YAAYI"} \ No newline at end of file diff --git a/amd/src/vertexcachestatus.js b/amd/src/vertexcachestatus.js new file mode 100644 index 0000000..c279271 --- /dev/null +++ b/amd/src/vertexcachestatus.js @@ -0,0 +1,142 @@ +// 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 . + +/** + * Module rendering the warning box to inform the users about misleading AI results. + * + * @module local_ai_manager/vertexcachestatus + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Templates from 'core/templates'; +import {call as fetchMany} from 'core/ajax'; +import {alert as alertModal, exception as displayException} from 'core/notification'; +import {getString} from 'core/str'; + +/** + * Fetches the current cache status of the specified service account. + * + * @param {string} serviceaccountinfo the stringified JSON with the service account info + */ +const fetchCurrentCacheStatus = (serviceaccountinfo) => fetchMany([{ + methodname: 'local_ai_manager_vertex_cache_status', + args: { + serviceaccountinfo + } +}])[0]; + +/** + * Updates the current cache status. + * + * @param {string} serviceaccountinfo the stringified JSON with the service account info + * @param {boolean} newstatus true if the cache should be enabled, false if it should be disabled + */ +const setCurrentCacheStatus = (serviceaccountinfo, newstatus) => fetchMany([{ + methodname: 'local_ai_manager_vertex_cache_status', + args: { + serviceaccountinfo, + newstatus + } +}])[0]; + +/** + * Controls and renders the Google Vertex AI cache status elements. + * + * @param {string} selector the CSS selector of the status element to operate on + */ +export const init = async(selector) => { + const statusElement = document.querySelector(selector); + const refreshButton = statusElement.querySelector('[data-action="refresh"]'); + const enableCachingButton = statusElement.querySelector('[data-action="enablecaching"]'); + const disableCachingButton = statusElement.querySelector('[data-action="disablecaching"]'); + const serviceaccountinfoTextArea = document.getElementById('id_serviceaccountjson'); + let serviceaccountinfo = serviceaccountinfoTextArea.value; + // We want to keep track of the current serviceaccountinfo data, also if the user changes it. + serviceaccountinfoTextArea.addEventListener('input', (event) => { + serviceaccountinfo = event.target.value; + }); + + refreshButton.addEventListener('click', async(event) => { + event.preventDefault(); + await updateCachingStatusDisplay(serviceaccountinfo, statusElement); + }); + + if (enableCachingButton) { + enableCachingButton.addEventListener('click', async(event) => { + event.preventDefault(); + enableCachingButton.disabled = true; + await updateCachingStatus(serviceaccountinfo, statusElement, true); + }); + } + if (disableCachingButton) { + disableCachingButton.addEventListener('click', async(event) => { + event.preventDefault(); + disableCachingButton.disabled = true; + await updateCachingStatus(serviceaccountinfo, statusElement, false); + }); + } +}; + +/** + * Updates the caching status display. + * + * @param {string} serviceaccountinfo the stringified JSON with the service account info + * @param {string} statusElement the HTML element to operate on + */ +const updateCachingStatusDisplay = async(serviceaccountinfo, statusElement) => { + let queryResult = null; + try { + queryResult = await fetchCurrentCacheStatus(serviceaccountinfo); + } catch (error) { + await displayException(error); + return; + } + if (queryResult.code !== 200) { + const errorTitleString = await getString('vertex_error_cachestatus', 'local_ai_manager'); + await alertModal(errorTitleString, queryResult.error); + } + const templateContext = { + cachingEnabled: queryResult.cachingEnabled, + noStatus: false + }; + + const {html, js} = await Templates.renderForPromise('local_ai_manager/vertexcachestatus', templateContext); + Templates.replaceNode(statusElement, html, js); +}; + +/** + * Updates the caching status and updates the DOM to reflect the current state. + * + * @param {string} serviceaccountinfo the stringified JSON with the service account info + * @param {string} statusElement the HTML element to operate on + * @param {boolean} newstatus the status to set the caching configuration to (true or false) + */ +const updateCachingStatus = async(serviceaccountinfo, statusElement, newstatus) => { + let queryResult = null; + try { + queryResult = await setCurrentCacheStatus(serviceaccountinfo, newstatus); + } catch (error) { + await displayException(error); + return; + } + if (queryResult.code !== 200) { + const errorTitleString = await getString('vertex_error_cachestatus', 'local_ai_manager'); + await alertModal(errorTitleString, queryResult.error); + return; + } + await updateCachingStatusDisplay(serviceaccountinfo, statusElement); +}; diff --git a/classes/base_connector.php b/classes/base_connector.php index c521cd5..053fe34 100644 --- a/classes/base_connector.php +++ b/classes/base_connector.php @@ -159,8 +159,8 @@ public function get_available_options(): array { */ public function make_request(array $data): request_response { $client = new http_client([ - 'timeout' => get_config('local_ai_manager', 'requesttimeout'), - 'verify' => !empty(get_config('local_ai_manager', 'verifyssl')), + 'timeout' => get_config('local_ai_manager', 'requesttimeout'), + 'verify' => !empty(get_config('local_ai_manager', 'verifyssl')), ]); $options['headers'] = $this->get_headers(); @@ -177,7 +177,8 @@ public function make_request(array $data): request_response { $return = request_response::create_from_error( $response->getStatusCode(), get_string('error_sendingrequestfailed', 'local_ai_manager'), - $response->getBody(), + $response->getBody()->getContents(), + $response->getBody() ); } return $return; @@ -231,7 +232,8 @@ final protected function create_error_response_from_exception(ClientExceptionInt if (method_exists($exception, 'getResponse') && !empty($exception->getResponse())) { $debuginfo .= $exception->getResponse()->getBody()->getContents(); } - return request_response::create_from_error($exception->getCode(), $message, $debuginfo); + return request_response::create_from_error($exception->getCode(), $message, $debuginfo, + $exception->getResponse()->getBody()); } /** diff --git a/classes/base_instance.php b/classes/base_instance.php index 283b09d..9e4948b 100644 --- a/classes/base_instance.php +++ b/classes/base_instance.php @@ -133,6 +133,7 @@ final public function load(): void { */ final public function store(): void { global $DB; + $clock = \core\di::get(\core\clock::class); $record = new stdClass(); $record->name = $this->name; $record->tenant = $this->tenant; @@ -146,13 +147,14 @@ final public function store(): void { $record->customfield3 = $this->customfield3; $record->customfield4 = $this->customfield4; $record->customfield5 = $this->customfield5; + $currenttime = $clock->time(); + $record->timemodified = $currenttime; if (is_null($this->record)) { - $record->timecreated = time(); + $record->timecreated = $currenttime; $record->id = $DB->insert_record('local_ai_manager_instance', $record); $this->id = $record->id; } else { $record->id = $this->id; - $record->timemodified = time(); $DB->update_record('local_ai_manager_instance', $record); } $this->record = $record; @@ -517,8 +519,10 @@ final public function edit_form_definition(\MoodleQuickForm $mform, array $custo */ final public function store_formdata(stdClass $data): void { $this->set_name(trim($data->name)); - $this->set_endpoint(trim($data->endpoint)); - $this->set_apikey(trim($data->apikey)); + if (!empty($data->endpoint)) { + $this->set_endpoint(trim($data->endpoint)); + } + $this->set_apikey(!empty($data->apikey) ? trim($data->apikey) : ''); $this->set_connector($data->connector); $this->set_tenant(trim($data->tenant)); if (empty($data->model)) { @@ -555,7 +559,9 @@ final public function validation(array $data, array $files): array { if (empty($data['name'])) { $errors['name'] = get_string('formvalidation_editinstance_name', 'local_ai_manager'); } - if (str_starts_with($data['endpoint'], 'http://') && !str_starts_with($data['endpoint'], 'https://')) { + if (!empty($data['endpoint']) + && str_starts_with($data['endpoint'], 'http://') + && !str_starts_with($data['endpoint'], 'https://')) { $errors['endpoint'] = get_string('formvalidation_editinstance_endpointnossl', 'local_ai_manager'); } return $errors + $this->extend_validation($data, $files); diff --git a/classes/external/vertex_cache_status.php b/classes/external/vertex_cache_status.php new file mode 100644 index 0000000..0b6cf7a --- /dev/null +++ b/classes/external/vertex_cache_status.php @@ -0,0 +1,109 @@ +. + +namespace local_ai_manager\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; +use local_ai_manager\local\aitool_option_vertexai_authhandler; + +/** + * Web service to check and update the Google Vertex AI cache status. + * + * @package local_ai_manager + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class vertex_cache_status extends external_api { + /** + * Describes the parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'serviceaccountinfo' => new external_value(PARAM_RAW, + 'The JSON string containing the service account information of the used Google Account', + VALUE_REQUIRED), + 'newstatus' => new external_value(PARAM_BOOL, + 'The status to which the caching config should be set to', + VALUE_DEFAULT, + null), + ]); + } + + /** + * Retrieve the purpose config. + * + * @param string $serviceaccountinfo The service account info stringified JSON + * @return array associative array containing the result of the request + */ + public static function execute(string $serviceaccountinfo, ?bool $newstatus = null): array { + [ + 'serviceaccountinfo' => $serviceaccountinfo, + 'newstatus' => $newstatus, + ] = self::validate_parameters(self::execute_parameters(), + [ + 'serviceaccountinfo' => $serviceaccountinfo, + 'newstatus' => $newstatus, + ]); + $context = \context_system::instance(); + self::validate_context($context); + require_capability('local/ai_manager:managevertexcache', $context); + + $vertexaiauthhandler = new aitool_option_vertexai_authhandler(0, $serviceaccountinfo); + if (!is_null($newstatus)) { + try { + $cachingchangeresult = $vertexaiauthhandler->set_google_cache_status($newstatus); + } catch (\moodle_exception $exception) { + return ['code' => 500, 'error' => $exception->getMessage()]; + } + return $cachingchangeresult ? ['code' => 200, 'cachingstatus' => $newstatus] : + ['code' => 500, 'error' => 'COULD NOT SET THE CACHING STATUS']; + } else { + // Variable $newstatus is null, so we just want to query and return the result. + try { + $currentcachingstatus = $vertexaiauthhandler->get_google_cache_status(); + } catch (\moodle_exception $exception) { + return ['code' => 500, 'error' => $exception->getMessage()]; + } + return ['code' => 200, 'cachingEnabled' => $currentcachingstatus]; + } + } + + /** + * Describes the return structure of the service. + * + * @return external_single_structure the return structure + */ + public static function execute_returns(): external_single_structure { + $singlestructuredefinition = []; + $singlestructuredefinition['code'] = new external_value(PARAM_INT, + 'Status code of the request', + VALUE_REQUIRED); + $singlestructuredefinition['cachingEnabled'] = new external_value(PARAM_BOOL, + 'If the Google Vertex AI cache is enabled', VALUE_OPTIONAL); + $singlestructuredefinition['error'] = new external_value(PARAM_TEXT, + 'Error message if there is an error', VALUE_OPTIONAL); + return new external_single_structure( + $singlestructuredefinition, + 'Object containing the tools configured for each purpose' + ); + } +} diff --git a/classes/local/aitool_option_vertexai.php b/classes/local/aitool_option_vertexai.php new file mode 100644 index 0000000..2c9bb12 --- /dev/null +++ b/classes/local/aitool_option_vertexai.php @@ -0,0 +1,105 @@ +. + +namespace local_ai_manager\local; + +use stdClass; + +/** + * Helper class for providing the necessary extension functions to implement the Google OAuth authentication for access to + * Google's Vertex AI. + * + * @package local_ai_manager + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class aitool_option_vertexai { + + /** + * Extends the form definition of the edit instance form by adding the Vertex AI options. + * + * @param \MoodleQuickForm $mform the mform object + */ + public static function extend_form_definition(\MoodleQuickForm $mform): void { + global $OUTPUT; + $mform->freeze('endpoint'); + $mform->addElement('textarea', 'serviceaccountjson', + get_string('serviceaccountjson', 'local_ai_manager'), ['rows' => '20']); + $vertexcachestatushtml = $OUTPUT->render_from_template('local_ai_manager/vertexcachestatus', ['noStatus' => true]); + $mform->addElement('static', 'vertexcachestatus', + get_string('vertexcachestatus', 'local_ai_manager'), + $vertexcachestatushtml, ['class' => 'mw-100']); + } + + /** + * Adds the Vertex AI data to the form data to be passed to the form when loading. + * + * @param string $serviceaccountjson the service account JSON string + * @return stdClass the object to pass to the form when loading + */ + public static function add_vertexai_to_form_data(string $serviceaccountjson): stdClass { + $data = new stdClass(); + $data->serviceaccountjson = $serviceaccountjson; + return $data; + } + + /** + * Extract the service account JSON and calculate the new base endpoint from the form data submitted by the form. + * + * @param stdClass $data the form data after submission + * @return array array of the service account JSON and the calculated endpoint + */ + public static function extract_vertexai_to_store(stdClass $data): array { + $serviceaccountjson = trim($data->serviceaccountjson); + $serviceaccountinfo = json_decode($serviceaccountjson); + $projectid = $serviceaccountinfo->project_id; + + $baseendpoint = 'https://europe-west3-aiplatform.googleapis.com/v1/projects/' . $projectid + . '/locations/europe-west3/publishers/google/models/' + . $data->model; + return [$serviceaccountjson, $baseendpoint]; + } + + /** + * Validation function for the Vertex AI option when form is being submitted. + * + * @param array $data the data being submitted by the form + * @return array associative array ['mformelementname' => 'error string'] if there are validation errors, otherwise empty array + */ + public static function validate_vertexai(array $data): array { + $errors = []; + if (empty($data['serviceaccountjson'])) { + $errors['serviceaccountjson'] = get_string('error_vertexai_serviceaccountjsonempty', 'local_ai_manager'); + return $errors; + } + + $serviceaccountinfo = json_decode(trim($data['serviceaccountjson'])); + if (is_null($serviceaccountinfo)) { + $errors['serviceaccountjson'] = get_string('error_vertexai_serviceaccountjsoninvalid', 'local_ai_manager'); + } else { + foreach (['private_key_id', 'private_key', 'client_email'] as $field) { + if (!property_exists($serviceaccountinfo, $field)) { + $errors['serviceaccountjson'] = + get_string('error_vertexai_serviceaccountjsoninvalidmissing', 'local_ai_manager', $field); + break; + } + } + } + + return $errors; + } +} diff --git a/classes/local/aitool_option_vertexai_authhandler.php b/classes/local/aitool_option_vertexai_authhandler.php new file mode 100644 index 0000000..dfc2104 --- /dev/null +++ b/classes/local/aitool_option_vertexai_authhandler.php @@ -0,0 +1,229 @@ +. + +namespace local_ai_manager\local; + +use core\http_client; +use Firebase\JWT\JWT; +use Psr\Http\Client\ClientExceptionInterface; + +/** + * Helper class for providing the necessary extension functions to implement the authentication with Google OAuth for an AI tool. + * + * @package local_ai_manager + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class aitool_option_vertexai_authhandler { + + /** + * Constructor for the auth handler. + */ + public function __construct( + /** @var int The ID of the instance being used. Will be used as key for the cache handling. */ + private readonly int $instanceid, + /** @var string The serviceaccountinfo stringified JSON */ + private readonly string $serviceaccountinfo + ) { + } + + /** + * Retrieves a fresh access token from the Google oauth endpoint. + * + * @return array of the form ['access_token' => 'xxx', 'expires' => 1730805678] containing the access token and the time at + * which the token expires. If there has been an error, the array is of the form + * ['error' => 'more detailed info about the error'] + */ + public function retrieve_access_token(): array { + $clock = \core\di::get(\core\clock::class); + $serviceaccountinfo = json_decode($this->serviceaccountinfo); + $kid = $serviceaccountinfo->private_key_id; + $privatekey = $serviceaccountinfo->private_key; + $clientemail = $serviceaccountinfo->client_email; + $jwtpayload = [ + 'iss' => $clientemail, + 'sub' => $clientemail, + 'scope' => 'https://www.googleapis.com/auth/cloud-platform', + 'aud' => 'https://oauth2.googleapis.com/token', + 'iat' => $clock->time(), + 'exp' => $clock->time() + HOURSECS, + ]; + $jwt = JWT::encode($jwtpayload, $privatekey, 'RS256', null, ['kid' => $kid]); + + $client = new http_client([ + 'timeout' => get_config('local_ai_manager', 'requesttimeout'), + ]); + $options['query'] = [ + 'assertion' => $jwt, + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + ]; + + try { + $response = $client->post('https://oauth2.googleapis.com/token', $options); + } catch (ClientExceptionInterface $exception) { + return ['error' => $exception->getMessage()]; + } + if ($response->getStatusCode() === 200) { + $content = $response->getBody()->getContents(); + if (empty($content)) { + return ['error' => 'Empty response']; + } + $content = json_decode($content, true); + if (empty($content['access_token'])) { + return ['error' => 'Response does not contain "access_token" key']; + } + return [ + 'access_token' => $content['access_token'], + // We set the expiry time of the access token and reduce it by 10 seconds to avoid some errors caused + // by different clocks on different servers, latency etc. + 'expires' => $clock->time() + intval($content['expires_in']) - 10, + ]; + } else { + return ['error' => 'Response status code is not OK 200, but ' . $response->getStatusCode() . ': ' . + $response->getBody()->getContents()]; + } + } + + /** + * Gets an access token for accessing Vertex AI endpoints. + * + * This will check if the cached access token still has not expired. If cache is empty or the token has expired + * a new access token will be fetched by calling {@see self::retrieve_access_token} and the new token will be stored + * in the cache. + * + * @return string the access token as string, empty if no + * @throws \moodle_exception if there is an error retrieving the access token. + */ + public function get_access_token(): string { + $clock = \core\di::get(\core\clock::class); + $authcache = \cache::make('local_ai_manager', 'googleauth'); + $cachedauthinfo = $authcache->get($this->instanceid); + if (empty($cachedauthinfo) || json_decode($cachedauthinfo)->expires < $clock->time()) { + $authinfo = $this->retrieve_access_token(); + if (!empty($authinfo['error'])) { + throw new \moodle_exception('Error retrieving access token', '', '', '', $authinfo['error']); + } + $cachedauthinfo = json_encode($authinfo); + $authcache->set($this->instanceid, $cachedauthinfo); + $accesstoken = $authinfo['access_token']; + } else { + $accesstoken = json_decode($cachedauthinfo, true)['access_token']; + } + return $accesstoken; + } + + /** + * Refreshes the current access token. + * + * Clears the existing access token and retrieves a new one by invoking {@see self::get_access_token}. + * + * @return string the newly fetched access token as a string + */ + public function refresh_access_token(): string { + $this->clear_access_token(); + return $this->get_access_token(); + } + + /** + * Clears the access token from the cache for the current instance. + */ + public function clear_access_token(): void { + $authcache = \cache::make('local_ai_manager', 'googleauth'); + $authcache->delete($this->instanceid); + } + + /** + * Regenerate a token if necessary. + * + * To check if it's necessary you need to pass over a request_response object that contains an answer from the API that you + * tried to access with an access token before. If the request response shows that the token was expired, it will be + * regenerated. You then can get it by calling {@see self::get_access_token()}. + * + * @param request_response $requestresponse the request_response object of the request before + */ + public function is_expired_accesstoken_reason_for_failing(request_response $requestresponse): bool { + if ($requestresponse->get_code() !== 401) { + return false; + } + // We need to reset the stream, so we can again read it. + $requestresponse->get_response()->rewind(); + $content = json_decode($requestresponse->get_response()->getContents(), true); + return !empty(array_filter($content['error']['details'], fn($details) => $details['reason'] === 'ACCESS_TOKEN_EXPIRED')); + } + + /** + * Retrieves and checks the cache status from Google's AI Platform. + * + * Makes an HTTP GET request to the AI Platform cache configuration endpoint + * using the project ID from the service account information. The method + * verifies if the cache is enabled by checking the 'disableCache' key in the + * response. + * + * @return bool true if the cache is enabled, false if the cache is disabled + * @throws \moodle_exception if the HTTP request to retrieve the cache status fails. + */ + public function get_google_cache_status(): bool { + $client = new http_client([ + 'timeout' => get_config('local_ai_manager', 'requesttimeout'), + ]); + + $options['headers'] = [ + 'Authorization' => 'Bearer ' . $this->get_access_token(), + ]; + + $serviceaccountinfo = json_decode($this->serviceaccountinfo); + $projectid = trim($serviceaccountinfo->project_id); + + $response = $client->get('https://europe-west3-aiplatform.googleapis.com/v1beta1/projects/' . $projectid . '/cacheConfig', + $options); + if ($response->getStatusCode() !== 200) { + throw new \moodle_exception('Error retrieving cache status', '', '', '', $response->getBody()->getContents()); + } else { + $result = json_decode($response->getBody()->getContents(), true); + return !array_key_exists('disableCache', $result); + } + } + + /** + * Sets the Google cache status for the specified project. + * + * @param bool $status Determines whether the cache should be enabled or disabled. + * @return bool Returns true if the cache status was successfully set, false otherwise. + */ + public function set_google_cache_status(bool $status): bool { + $client = new http_client([ + 'timeout' => get_config('local_ai_manager', 'requesttimeout'), + ]); + $options['headers'] = [ + 'Authorization' => 'Bearer ' . $this->get_access_token(), + ]; + + $serviceaccountinfo = json_decode($this->serviceaccountinfo); + $projectid = trim($serviceaccountinfo->project_id); + + $data = [ + 'name' => 'projects/' . $projectid . '/cacheConfig', + 'disableCache' => !$status, + ]; + + $options['body'] = json_encode($data); + + $response = $client->patch('https://europe-west3-aiplatform.googleapis.com/v1beta1/projects/' . $projectid . '/cacheConfig', + $options); + return $response->getStatusCode() === 200; + } +} diff --git a/classes/local/prompt_response.php b/classes/local/prompt_response.php index 47e1c35..0624630 100644 --- a/classes/local/prompt_response.php +++ b/classes/local/prompt_response.php @@ -39,7 +39,7 @@ class prompt_response { private int $code; /** @var string If there has been an error, this variable contains the error message */ - private string $errormessage; + private string $errormessage = ''; /** @var string If there has been an error, this variable contains additional debugging information */ private string $debuginfo; diff --git a/classes/local/request_response.php b/classes/local/request_response.php index bf2aec2..b763de5 100644 --- a/classes/local/request_response.php +++ b/classes/local/request_response.php @@ -126,13 +126,18 @@ public function get_code(): int { * @param int $code the status code * @param string $errormessage the error message * @param string $debuginfo the debug info + * @param ?StreamInterface $rawresponse the raw response object, or null if not available * @return request_response the request_response object containing all information about the error */ - public static function create_from_error(int $code, string $errormessage, string $debuginfo): request_response { + public static function create_from_error(int $code, string $errormessage, string $debuginfo, + ?StreamInterface $rawresponse = null): request_response { $requestresponse = new self(); $requestresponse->set_code($code); $requestresponse->set_errormessage($errormessage); $requestresponse->set_debuginfo($debuginfo); + if (!empty($rawresponse)) { + $requestresponse->set_response($rawresponse); + } return $requestresponse; } diff --git a/classes/manager.php b/classes/manager.php index 975a64b..66a7de7 100644 --- a/classes/manager.php +++ b/classes/manager.php @@ -28,7 +28,6 @@ use context; use context_system; use core_plugin_manager; -use dml_exception; use local_ai_manager\event\get_ai_response_failed; use local_ai_manager\event\get_ai_response_succeeded; use local_ai_manager\local\config_manager; @@ -182,6 +181,10 @@ public function perform_request(string $prompttext, array $options = []): prompt return $promptresponse; } $promptcompletion = $this->connector->execute_prompt_completion($requestresult->get_response(), $options); + if (!empty($promptcompletion->get_errormessage())) { + get_ai_response_failed::create_from_prompt_response($promptdata, $promptcompletion, $duration)->trigger(); + return $promptcompletion; + } if (!empty($options['forcenewitemid']) && !empty($options['component']) && !empty($options['contextid'] && !empty($options['itemid']))) { if ($DB->record_exists('local_ai_manager_request_log', diff --git a/db/access.php b/db/access.php index 4477985..fa3ae33 100755 --- a/db/access.php +++ b/db/access.php @@ -110,5 +110,16 @@ 'manager' => CAP_ALLOW, ], ], - + 'local/ai_manager:managevertexcache' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'user' => CAP_PREVENT, + 'guest' => CAP_PREVENT, + 'student' => CAP_PREVENT, + 'teacher' => CAP_PREVENT, + 'editingteacher' => CAP_PREVENT, + 'manager' => CAP_ALLOW, + ], + ], ]; diff --git a/db/caches.php b/db/caches.php index 1a60ae4..d4349f6 100644 --- a/db/caches.php +++ b/db/caches.php @@ -26,10 +26,10 @@ defined('MOODLE_INTERNAL') || die(); $definitions = [ - 'googlesynthesizevoices' => [ + 'googleauth' => [ 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, - 'simpledata' => false, + 'simpledata' => true, 'canuselocalstore' => false, ], ]; diff --git a/db/install.xml b/db/install.xml index c10cb1c..20ed5d6 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -14,11 +14,11 @@ - - - - - + + + + + diff --git a/db/services.php b/db/services.php index 99783fc..841c6ca 100755 --- a/db/services.php +++ b/db/services.php @@ -54,4 +54,12 @@ 'ajax' => true, 'capabilities' => 'local/ai_manager:use', ], + 'local_ai_manager_vertex_cache_status' => [ + 'classname' => 'local_ai_manager\external\vertex_cache_status', + 'description' => 'Fetch and update the Google Vertex AI caching status', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => '', + ], + ]; diff --git a/db/upgrade.php b/db/upgrade.php index edf987b..7d7b447 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -144,5 +144,39 @@ function xmldb_local_ai_manager_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2024092600, 'local', 'ai_manager'); } + if ($oldversion < 2024110501) { + + // Changing type of field customfield1 on table local_ai_manager_instance to text. + $table = new xmldb_table('local_ai_manager_instance'); + $field = new xmldb_field('customfield1', XMLDB_TYPE_TEXT, null, null, null, null, null, 'infolink'); + $dbman->change_field_type($table, $field); + $field = new xmldb_field('customfield2', XMLDB_TYPE_TEXT, null, null, null, null, null, 'customfield1'); + $dbman->change_field_type($table, $field); + $field = new xmldb_field('customfield3', XMLDB_TYPE_TEXT, null, null, null, null, null, 'customfield2'); + $dbman->change_field_type($table, $field); + $field = new xmldb_field('customfield4', XMLDB_TYPE_TEXT, null, null, null, null, null, 'customfield3'); + $dbman->change_field_type($table, $field); + $field = new xmldb_field('customfield5', XMLDB_TYPE_TEXT, null, null, null, null, null, 'customfield4'); + $dbman->change_field_type($table, $field); + + // Ai_manager savepoint reached. + upgrade_plugin_savepoint(true, 2024110501, 'local', 'ai_manager'); + } + + if ($oldversion < 2024120200) { + + $rs = $DB->get_recordset('local_ai_manager_instance', ['connector' => 'gemini']); + foreach ($rs as $record) { + $record->customfield2 = 'googleai'; + $record->model = str_replace('-latest', '', $record->model); + $record->endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $record->model . ':generateContent'; + $DB->update_record('local_ai_manager_instance', $record); + } + $rs->close(); + + // AI manager savepoint reached. + upgrade_plugin_savepoint(true, 2024120200, 'local', 'ai_manager'); + } + return true; } diff --git a/lang/de/local_ai_manager.php b/lang/de/local_ai_manager.php index a36893e..367a14b 100644 --- a/lang/de/local_ai_manager.php +++ b/lang/de/local_ai_manager.php @@ -52,6 +52,7 @@ $string['assignrole'] = 'Rolle zuweisen'; $string['basicsettings'] = 'Grundeinstellungen'; $string['basicsettingsdesc'] = 'Grundeinstellungen des AI-Managers konfigurieren'; +$string['cachedef_googleauth'] = 'Cache für Google-OAuth2-Access-Token'; $string['configure_instance'] = 'KI-Tool-Instanzen konfigurieren'; $string['configureaitool'] = 'KI-Tool konfigurieren'; $string['configurepurposes'] = 'Einsatzzwecke konfigurieren'; @@ -81,6 +82,9 @@ $string['error_unavailable_selection'] = 'Dieses Tool ist nur verfügbar, wenn Text markiert wurde.'; $string['error_userlocked'] = 'Ihr Nutzer wurde von Ihrem Tenant-Manager gesperrt.'; $string['error_usernotconfirmed'] = 'Für die Nutzung müssen die Nutzungsbedingungen akzeptiert werden.'; +$string['error_vertexai_serviceaccountjsonempty'] = 'Sie müssen den Inhalt der JSON-Datei, die Sie beim Anlegen des Service-Accounts in Ihrer Google-Cloud-Console heruntergeladen haben, einfügen.'; +$string['error_vertexai_serviceaccountjsoninvalid'] = 'Ungültiges Format. Es muss sich um gültiges JSON handeln.'; +$string['error_vertexai_serviceaccountjsoninvalidmissing'] = 'Ungültiges Format. Es fehlt der Eintrag "{$a}".'; $string['exception_curl'] = 'Es ist ein Fehler bei der Verbindung zum externen KI-Tool aufgetreten.'; $string['exception_curl28'] = 'Die API hat zu lange gebraucht, um Ihre Anfrage zu verarbeiten, oder konnte nicht in angemessener Zeit erreicht werden.'; $string['exception_default'] = 'Ein allgemeiner Fehler ist aufgetreten, während versucht wurde, die Anfrage an das KI-Tool zu senden.'; @@ -152,6 +156,7 @@ $string['role_unlimited'] = 'Unbeschränkte Rolle'; $string['select_tool_for_purpose'] = 'Einsatzzweck "{$a}"'; $string['selecteduserscount'] = '{$a} ausgewählt'; +$string['serviceaccountjson'] = 'Inhalt der JSON-Datei des Google-Serviceaccounts'; $string['settingsgeneral'] = 'Allgemein'; $string['small'] = 'klein'; $string['squared'] = 'quadratisch'; @@ -206,4 +211,11 @@ $string['userwithusageonlyshown'] = 'Die Tabelle zeigt nur Benutzer, die diesen Einsatzzweck bereits genutzt haben.'; $string['verifyssl'] = 'SSL-Zertifikate verifizieren'; $string['verifyssldesc'] = 'Wenn aktiviert, werden Verbindungen zu externen KI-Tools nur dann hergestellt, wenn die Zertifikate verifiziert werden können. Diese Option sollte in Produktionsumgebungen nicht deaktiviert werden!'; +$string['vertex_cachingdisabled'] = 'Caching deaktiviert'; +$string['vertex_cachingenabled'] = 'Caching aktiviert'; +$string['vertex_disablecaching'] = 'Caching deaktivieren'; +$string['vertex_enablecaching'] = 'Caching aktivieren'; +$string['vertex_error_cachestatus'] = 'Fehler beim Abfragen/Ändern der Vertex-AI-Cache-Konfiguration'; +$string['vertex_nocachestatus'] = 'Klicken Sie auf den Neu-Laden-Button, um den aktuellen Caching-Status von Vertex AI abzufragen.'; +$string['vertexcachestatus'] = 'Cache-Status von Vertex AI abfragen und ändern'; $string['within'] = 'innerhalb von'; diff --git a/lang/en/local_ai_manager.php b/lang/en/local_ai_manager.php index dcd738f..6523df3 100644 --- a/lang/en/local_ai_manager.php +++ b/lang/en/local_ai_manager.php @@ -52,6 +52,7 @@ $string['assignrole'] = 'Assign role'; $string['basicsettings'] = 'Basic settings'; $string['basicsettingsdesc'] = 'Configure basic settings for the AI manager plugin'; +$string['cachedef_googleauth'] = 'Cache for Google OAuth2 access token'; $string['configure_instance'] = 'Configure AI Tool Instances'; $string['configureaitool'] = 'Configure AI tool'; $string['configurepurposes'] = 'Configure the purposes'; @@ -81,6 +82,9 @@ $string['error_unavailable_selection'] = 'This tool is only available if no text has been selected.'; $string['error_userlocked'] = 'Your user has been locked by your tenant manager.'; $string['error_usernotconfirmed'] = 'You have not accepted the terms of use yet.'; +$string['error_vertexai_serviceaccountjsonempty'] = 'You need to paste the content of the JSON file that you downloaded when creating the service account in your Google Cloud Console.'; +$string['error_vertexai_serviceaccountjsoninvalid'] = 'Invalid format. Has to be valid JSON.'; +$string['error_vertexai_serviceaccountjsoninvalidmissing'] = 'Invalid format. The entry "{$a}" is missing.'; $string['exception_curl'] = 'A connection error to the external API endpoint has occurred'; $string['exception_curl28'] = 'The API took too long to process your request or could not be reached in a reasonable time'; $string['exception_default'] = 'A general error occurred while trying to send the request to the AI tool'; @@ -152,6 +156,7 @@ $string['role_unlimited'] = 'unlimited role'; $string['select_tool_for_purpose'] = 'Purpose {$a}'; $string['selecteduserscount'] = '{$a} selected'; +$string['serviceaccountjson'] = 'Content of the JSON file of the Google service account'; $string['small'] = 'small'; $string['squared'] = 'squared'; $string['statistics_for'] = 'Statistic for {$a}'; @@ -206,4 +211,11 @@ $string['userwithusageonlyshown'] = 'Only users who already have used this purpose are being shown in this table.'; $string['verifyssl'] = 'Verify SSL certificates'; $string['verifyssldesc'] = 'If enabled, connections to the AI tools will only be established if the certificates can properly be verified. Only recommended to disable for development use!'; +$string['vertex_cachingdisabled'] = 'Caching disabled'; +$string['vertex_cachingenabled'] = 'Caching enabled'; +$string['vertex_disablecaching'] = 'Disable Caching'; +$string['vertex_enablecaching'] = 'Enable Caching'; +$string['vertex_error_cachestatus'] = 'Error while querying/updating the Vertex AI caching configuration'; +$string['vertex_nocachestatus'] = 'Click the refresh button to query the current Vertex AI cache status.'; +$string['vertexcachestatus'] = 'Query and change Vertex AI cache status'; $string['within'] = 'in'; diff --git a/styles.css b/styles.css index d72c427..4ffb0b1 100644 --- a/styles.css +++ b/styles.css @@ -142,3 +142,15 @@ body.limitcontentwidth #page-content { .dark .local_ai_manager-info-warning .local_ai_manager-info-warning-link { color: #60616d; } + +.local_ai_manager-caching_enabled { + color: #f00; +} + +.local_ai_manager-caching_disabled { + color: #00bf00; +} + +#page-local-ai_manager-edit_instance [data-name="vertexcachestatus"] { + width: 100%; +} \ No newline at end of file diff --git a/templates/vertexcachestatus.mustache b/templates/vertexcachestatus.mustache new file mode 100644 index 0000000..f5bb2f5 --- /dev/null +++ b/templates/vertexcachestatus.mustache @@ -0,0 +1,50 @@ +{{! + 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 . +}} +{{! + @template local_ai_manager/vertexcachestatus + + Template for showing and editing the Google Vertex AI cache status. + + Example context (json): + { + "noStatus": false, + "cachingEnabled": true + } +}} +
+ {{#noStatus}} +
{{#str}}vertex_nocachestatus, local_ai_manager{{/str}}
+ {{/noStatus}} + {{^noStatus}} +
+ {{#cachingEnabled}} +
{{#str}}vertex_cachingenabled, local_ai_manager{{/str}}
+ + {{/cachingEnabled}} + {{^cachingEnabled}} +
{{#str}}vertex_cachingdisabled, local_ai_manager{{/str}}
+ + {{/cachingEnabled}} +
+ {{/noStatus}} +
+
+{{#js}} + require(['local_ai_manager/vertexcachestatus'], function(vertexcachestatus) { + vertexcachestatus.init('#local_ai_manager-vertexcachestatus-{{uniqid}}'); + }); +{{/js}} diff --git a/tests/ai_manager_utils_test.php b/tests/ai_manager_utils_test.php index 536a583..27b41bb 100644 --- a/tests/ai_manager_utils_test.php +++ b/tests/ai_manager_utils_test.php @@ -16,6 +16,8 @@ namespace local_ai_manager; +use Firebase\JWT\JWT; +use local_ai_manager\local\aitool_option_vertexai_authhandler; use stdClass; /** diff --git a/tools/dalle/classes/instance.php b/tools/dalle/classes/instance.php index eba72bb..59a7216 100644 --- a/tools/dalle/classes/instance.php +++ b/tools/dalle/classes/instance.php @@ -18,7 +18,6 @@ use local_ai_manager\base_instance; use local_ai_manager\local\aitool_option_azure; -use local_ai_manager\local\aitool_option_temperature; use stdClass; /** @@ -71,7 +70,6 @@ protected function extend_store_formdata(stdClass $data): void { #[\Override] protected function extend_validation(array $data, array $files): array { $errors = []; - $errors = array_merge($errors, aitool_option_temperature::validate_temperature($data)); $errors = array_merge($errors, aitool_option_azure::validate_azure_options($data)); return $errors; } diff --git a/tools/gemini/classes/connector.php b/tools/gemini/classes/connector.php index 2be9929..33f56e5 100644 --- a/tools/gemini/classes/connector.php +++ b/tools/gemini/classes/connector.php @@ -16,7 +16,9 @@ namespace aitool_gemini; +use local_ai_manager\local\aitool_option_vertexai_authhandler; use local_ai_manager\local\prompt_response; +use local_ai_manager\local\request_response; use local_ai_manager\local\unit; use local_ai_manager\local\usage; use Psr\Http\Message\StreamInterface; @@ -31,15 +33,18 @@ */ class connector extends \local_ai_manager\base_connector { + /** @var string The access token to use for authentication against the Google API endpoint. */ + private string $accesstoken = ''; + #[\Override] public function get_models_by_purpose(): array { - $textmodels = ['gemini-1.0-pro-latest', 'gemini-1.0-pro-vision-latest', 'gemini-1.5-flash-latest', 'gemini-1.5-pro-latest']; + $textmodels = ['gemini-1.0-pro', 'gemini-1.0-pro-vision', 'gemini-1.5-flash', 'gemini-1.5-pro']; return [ 'chat' => $textmodels, 'feedback' => $textmodels, 'singleprompt' => $textmodels, 'translate' => $textmodels, - 'itt' => ['gemini-1.5-pro-latest', 'gemini-1.5-flash-latest'], + 'itt' => ['gemini-1.5-pro', 'gemini-1.5-flash'], ]; } @@ -148,12 +153,41 @@ public function has_customvalue2(): bool { protected function get_headers(): array { $headers = parent::get_headers(); if (in_array('Authorization', array_keys($headers))) { - unset($headers['Authorization']); - $headers['x-goog-api-key'] = $this->get_api_key(); + if ($this->instance->get_customfield2() === \aitool_gemini\instance::GOOGLE_BACKEND_GOOGLEAI) { + unset($headers['Authorization']); + $headers['x-goog-api-key'] = $this->get_api_key(); + } else { + $headers['Authorization'] = 'Bearer ' . $this->accesstoken; + } } return $headers; } + #[\Override] + public function make_request(array $data): request_response { + if ($this->instance->get_customfield2() === instance::GOOGLE_BACKEND_VERTEXAI) { + $vertexaiauthhandler = + new aitool_option_vertexai_authhandler($this->instance->get_id(), $this->instance->get_customfield3()); + try { + $this->accesstoken = $vertexaiauthhandler->get_access_token(); + } catch (\moodle_exception $exception) { + return request_response::create_from_error(0, $exception->getMessage(), $exception->getTraceAsString()); + } + $requestresponse = parent::make_request($data); + // We keep track of the time the cached access token expires. However, due latency, different clocks + // on different servers etc. we could end up sending a request with an actually expired access token. + // In this case we refresh our access token and re-submit the request ONE TIME. + if ($vertexaiauthhandler->is_expired_accesstoken_reason_for_failing($requestresponse)) { + try { + $vertexaiauthhandler->refresh_access_token(); + } catch (\moodle_exception $exception) { + return request_response::create_from_error(0, $exception->getMessage(), $exception->getTraceAsString()); + } + } + } + return parent::make_request($data); + } + #[\Override] public function allowed_mimetypes(): array { // We use the inline_data for sending data to the API, so we basically support every format. diff --git a/tools/gemini/classes/instance.php b/tools/gemini/classes/instance.php index acf4c99..7e62391 100644 --- a/tools/gemini/classes/instance.php +++ b/tools/gemini/classes/instance.php @@ -18,6 +18,7 @@ use local_ai_manager\base_instance; use local_ai_manager\local\aitool_option_temperature; +use local_ai_manager\local\aitool_option_vertexai; use stdClass; /** @@ -30,18 +31,37 @@ */ class instance extends base_instance { + /** @var string Constant for declaring that Gemini should be used over the open Google AI API. */ + const GOOGLE_BACKEND_GOOGLEAI = 'googleai'; + + /** @var string Constant for declaring that Gemini should be used over Vertex AI. */ + const GOOGLE_BACKEND_VERTEXAI = 'vertexai'; + #[\Override] protected function extend_form_definition(\MoodleQuickForm $mform): void { - $mform->setDefault('endpoint', 'https://generativelanguage.googleapis.com/v1beta/models'); - $mform->freeze('endpoint'); + $mform->addElement('select', 'googlebackend', get_string('googlebackend', 'aitool_gemini'), + [ + self::GOOGLE_BACKEND_GOOGLEAI => get_string('googlebackendgoogleai', 'aitool_gemini'), + self::GOOGLE_BACKEND_VERTEXAI => get_string('googlebackendvertexai', 'aitool_gemini'), + ]); + aitool_option_vertexai::extend_form_definition($mform); + $mform->hideIf('serviceaccountjson', 'googlebackend', 'neq', 'vertexai'); + $mform->hideIf('apikey', 'googlebackend', 'eq', 'vertexai'); aitool_option_temperature::extend_form_definition($mform); } #[\Override] protected function get_extended_formdata(): stdClass { - $temperature = $this->get_customfield1(); $data = new stdClass(); + if ($this->get_customfield2() === self::GOOGLE_BACKEND_VERTEXAI) { + $data->googlebackend = self::GOOGLE_BACKEND_VERTEXAI; + $vertexaidata = aitool_option_vertexai::add_vertexai_to_form_data($this->get_customfield3()); + foreach ($vertexaidata as $key => $value) { + $data->{$key} = $value; + } + } + $temperature = $this->get_customfield1(); $temperaturedata = aitool_option_temperature::add_temperature_to_form_data($temperature); foreach ($temperaturedata as $key => $value) { $data->{$key} = $value; @@ -53,11 +73,26 @@ protected function get_extended_formdata(): stdClass { protected function extend_store_formdata(stdClass $data): void { $temperature = aitool_option_temperature::extract_temperature_to_store($data); $this->set_customfield1($temperature); + $this->set_customfield2($data->googlebackend); + if ($data->googlebackend === self::GOOGLE_BACKEND_VERTEXAI) { + [$serviceaccountjson, $baseendpoint] = aitool_option_vertexai::extract_vertexai_to_store($data); + $this->set_customfield3($serviceaccountjson); + $this->set_endpoint($baseendpoint . ':generateContent'); + } else { + $this->set_endpoint('https://generativelanguage.googleapis.com/v1beta/models/' . $this->get_model() . + ':generateContent'); + } } #[\Override] protected function extend_validation(array $data, array $files): array { - return aitool_option_temperature::validate_temperature($data); + $errors = []; + if ($data['googlebackend'] === self::GOOGLE_BACKEND_VERTEXAI) { + $errors = array_merge($errors, aitool_option_vertexai::validate_vertexai($data)); + } + $errors = array_merge($errors, aitool_option_temperature::validate_temperature($data)); + return $errors; + } /** @@ -68,9 +103,4 @@ protected function extend_validation(array $data, array $files): array { public function get_temperature(): float { return floatval($this->get_customfield1()); } - - #[\Override] - public function get_endpoint(): string { - return parent::get_endpoint() . '/' . $this->get_model() . ':generateContent'; - } } diff --git a/tools/gemini/lang/de/aitool_gemini.php b/tools/gemini/lang/de/aitool_gemini.php index 88bfcc9..ded22e0 100644 --- a/tools/gemini/lang/de/aitool_gemini.php +++ b/tools/gemini/lang/de/aitool_gemini.php @@ -24,5 +24,8 @@ */ $string['adddescription'] = 'Gemini, das KI-Modell von Google DeepMind, kombiniert fortschrittliche Sprachmodelle (LLMs) mit Tools und multimodalen Fähigkeiten.'; +$string['googlebackend'] = 'Zu nutzendes Google-Backend'; +$string['googlebackendgoogleai'] = 'Google AI'; +$string['googlebackendvertexai'] = 'Vertex AI'; $string['pluginname'] = 'Gemini'; $string['privacy:metadata'] = 'Das Subplugin des ai_manager Plugins "Gemini" speichert keine personenbezogenen Daten.'; diff --git a/tools/gemini/lang/en/aitool_gemini.php b/tools/gemini/lang/en/aitool_gemini.php index c556e40..6dbd4af 100644 --- a/tools/gemini/lang/en/aitool_gemini.php +++ b/tools/gemini/lang/en/aitool_gemini.php @@ -24,5 +24,8 @@ */ $string['adddescription'] = 'Gemini, Google DeepMind\'s AI model, combines advanced language models (LLMs) with tools and multimodal capabilities.'; +$string['googlebackend'] = 'Google Backend to use'; +$string['googlebackendgoogleai'] = 'Google AI'; +$string['googlebackendvertexai'] = 'Vertex AI'; $string['pluginname'] = 'Gemini'; $string['privacy:metadata'] = 'The local ai_manager tool subplugin "Gemini" does not store any personal data.'; diff --git a/tools/gemini/version.php b/tools/gemini/version.php index e6022c8..a29b91f 100644 --- a/tools/gemini/version.php +++ b/tools/gemini/version.php @@ -24,7 +24,7 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023121205; +$plugin->version = 2024120300; $plugin->requires = 2023042403; $plugin->release = '0.0.1'; $plugin->component = 'aitool_gemini'; diff --git a/tools/googlesynthesize/classes/connector.php b/tools/googlesynthesize/classes/connector.php index 37124e8..9326cc4 100644 --- a/tools/googlesynthesize/classes/connector.php +++ b/tools/googlesynthesize/classes/connector.php @@ -64,6 +64,7 @@ public function make_request(array $data): request_response { } else { $return = request_response::create_from_error($response->getStatusCode(), get_string('error_sendingrequestfailed', 'local_ai_manager'), + $response->getBody()->getContents(), $response->getBody() ); } @@ -161,7 +162,7 @@ public function get_available_options(): array { */ public function retrieve_available_voices(): array { $clock = \core\di::get(\core\clock::class); - $cache = \cache::make('local_ai_manager', 'googlesynthesizevoices'); + $cache = \cache::make('aitool_googlesynthesize', 'googlesynthesizevoices'); $voices = $cache->get('voices'); if ($voices) { $lastfetched = $cache->get('lastfetched'); diff --git a/tools/googlesynthesize/classes/instance.php b/tools/googlesynthesize/classes/instance.php index 9f67f5f..83d6e86 100644 --- a/tools/googlesynthesize/classes/instance.php +++ b/tools/googlesynthesize/classes/instance.php @@ -17,6 +17,8 @@ namespace aitool_googlesynthesize; use local_ai_manager\base_instance; +use local_ai_manager\local\aitool_option_temperature; +use local_ai_manager\local\aitool_option_vertexai; /** * Instance class for the connector instance of aitool_googlesynthesize. diff --git a/tools/googlesynthesize/db/caches.php b/tools/googlesynthesize/db/caches.php new file mode 100644 index 0000000..4fc1311 --- /dev/null +++ b/tools/googlesynthesize/db/caches.php @@ -0,0 +1,35 @@ +. + +/** + * Cache definitions. + * + * @package aitool_googlesynthesize + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$definitions = [ + 'googlesynthesizevoices' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => false, + 'canuselocalstore' => false, + ], +]; diff --git a/tools/googlesynthesize/lang/de/aitool_googlesynthesize.php b/tools/googlesynthesize/lang/de/aitool_googlesynthesize.php index 37481b4..5564c5b 100644 --- a/tools/googlesynthesize/lang/de/aitool_googlesynthesize.php +++ b/tools/googlesynthesize/lang/de/aitool_googlesynthesize.php @@ -24,5 +24,6 @@ */ $string['adddescription'] = 'Google Synthesize, oft im Kontext von “Google Text-to-Speech”, ist ein Werkzeug zur Umwandlung von Text in gesprochene Sprache.'; +$string['cachedef_googlesynthesizevoices'] = 'Cache zum Zwischenspeichern verfügbarer Stimmen des Synthesize-Dienstes'; $string['pluginname'] = 'Google Synthesize'; $string['privacy:metadata'] = 'Das Subplugin des ai_manager Plugins "Google Synthesize" speichert keine personenbezogenen Daten.'; diff --git a/tools/googlesynthesize/lang/en/aitool_googlesynthesize.php b/tools/googlesynthesize/lang/en/aitool_googlesynthesize.php index ba37286..07754d5 100644 --- a/tools/googlesynthesize/lang/en/aitool_googlesynthesize.php +++ b/tools/googlesynthesize/lang/en/aitool_googlesynthesize.php @@ -24,5 +24,6 @@ */ $string['adddescription'] = 'Google Synthesize, often in the context of "Google Text-to-Speech", is a tool for converting text into spoken language.'; +$string['cachedef_googlesynthesizevoices'] = 'Cache for storing available voices for the google synthesize service'; $string['pluginname'] = 'Google Synthesize'; $string['privacy:metadata'] = 'The local ai_manager tool subplugin "Google Synthesize" does not store any personal data.'; diff --git a/tools/googlesynthesize/version.php b/tools/googlesynthesize/version.php index e809443..1d6bb57 100644 --- a/tools/googlesynthesize/version.php +++ b/tools/googlesynthesize/version.php @@ -24,7 +24,7 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023121205; +$plugin->version = 2024110500; $plugin->requires = 2023042403; $plugin->release = '0.0.1'; $plugin->component = 'aitool_googlesynthesize'; diff --git a/tools/imagen/classes/connector.php b/tools/imagen/classes/connector.php new file mode 100644 index 0000000..fa3730a --- /dev/null +++ b/tools/imagen/classes/connector.php @@ -0,0 +1,181 @@ +. + +namespace aitool_imagen; + +use core\http_client; +use Firebase\JWT\JWT; +use local_ai_manager\base_connector; +use local_ai_manager\base_instance; +use local_ai_manager\local\aitool_option_vertexai_authhandler; +use local_ai_manager\local\prompt_response; +use local_ai_manager\local\request_response; +use local_ai_manager\local\unit; +use local_ai_manager\local\usage; +use local_ai_manager\manager; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Message\StreamInterface; + +/** + * Connector for Imagen. + * + * @package aitool_imagen + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class connector extends base_connector { + + /** @var string The access token to use for authentication against the Google imagen API endpoint. */ + private string $accesstoken = ''; + + #[\Override] + public function get_models_by_purpose(): array { + return [ + 'imggen' => ['imagegeneration@006', 'imagen-3.0-generate-001'], + ]; + } + + #[\Override] + public function get_prompt_data(string $prompttext, array $requestoptions): array { + $promptdata = [ + 'instances' => [ + [ + 'prompt' => $prompttext, + ], + ], + 'parameters' => [ + 'sampleCount' => 1, + 'safetySetting' => 'block_few', + 'language' => 'en', + 'aspectRatio' => $requestoptions['sizes'][0], + ], + ]; + + return $promptdata; + } + + #[\Override] + protected function get_headers(): array { + $headers = parent::get_headers(); + $headers['Authorization'] = 'Bearer ' . $this->accesstoken; + return $headers; + } + + #[\Override] + public function get_unit(): unit { + return unit::COUNT; + } + + #[\Override] + public function make_request(array $data): request_response { + // Currently, imagen does not support many languages. So we first translate the prompt into English and hardcode the + // language to "English" later on in the request options. + $translatemanager = new manager('translate'); + $translaterequestresult = $translatemanager->perform_request( + 'Translate the following words into English, only return the translated text: ' + . $data['instances'][0]['prompt']); + if ($translaterequestresult->get_code() !== 200) { + return request_response::create_from_error($translaterequestresult->get_code(), + get_string('err_translationfailed', 'aitool_imagen'), $translaterequestresult->get_debuginfo()); + } + $translatedprompt = $translaterequestresult->get_content(); + + // Subsitute the current prompt by the translated one. + $data['instances'][0]['prompt'] = $translatedprompt; + + $vertexaiauthhandler = + new aitool_option_vertexai_authhandler($this->instance->get_id(), $this->instance->get_customfield1()); + try { + // Composing the "Authorization" header is not that easy as just looking up a Bearer token in the database. + // So we here explicitly retrieve the access token from cache or the Google OAuth API and do some proper error handling. + // After we stored it in $this->accesstoken it can be properly set into the header by the self::get_headers method. + $this->accesstoken = $vertexaiauthhandler->get_access_token(); + } catch (\moodle_exception $exception) { + return request_response::create_from_error(0, $exception->getMessage(), $exception->getTraceAsString()); + } + $requestresponse = parent::make_request($data); + // We keep track of the time the cached access token expires. However, due latency, different clocks + // on different servers etc. we could end up sending a request with an actually expired access token. + // In this case we refresh our access token and re-submit the request ONE TIME. + if ($vertexaiauthhandler->is_expired_accesstoken_reason_for_failing($requestresponse)) { + try { + $vertexaiauthhandler->refresh_access_token(); + } catch (\moodle_exception $exception) { + return request_response::create_from_error(0, $exception->getMessage(), $exception->getTraceAsString()); + } + $requestresponse = parent::make_request($data); + } + return $requestresponse; + } + + #[\Override] + public function execute_prompt_completion(StreamInterface $result, array $options = []): prompt_response { + global $USER; + $content = json_decode($result->getContents(), true); + if (empty($content['predictions']) || !array_key_exists('bytesBase64Encoded', $content['predictions'][0])) { + return prompt_response::create_from_error(400, + get_string('err_predictionmissing', 'aitool_imagen'), ''); + } + $fs = get_file_storage(); + $fileinfo = [ + 'contextid' => \context_user::instance($USER->id)->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $options['itemid'], + 'filepath' => '/', + 'filename' => $options['filename'], + ]; + $file = $fs->create_file_from_string($fileinfo, base64_decode($content['predictions'][0]['bytesBase64Encoded'])); + + $filepath = \moodle_url::make_draftfile_url( + $file->get_itemid(), + $file->get_filepath(), + $file->get_filename() + )->out(); + + return prompt_response::create_from_result($this->instance->get_model(), new usage(1.0), $filepath); + } + + #[\Override] + public function get_available_options(): array { + $options['sizes'] = [ + ['key' => '1:1', 'displayname' => '1:1 (1536 x 1536)'], + ['key' => '9:16', 'displayname' => '9:16 (1152 x 2016)'], + ['key' => '16:9', 'displayname' => '16:9 (2016 x 1134)'], + ['key' => '3:4', 'displayname' => '3:4 (1344 x 1792)'], + ['key' => '4:3', 'displayname' => '4:3 (1792 x 1344)'], + ]; + return $options; + } + + #[\Override] + protected function get_custom_error_message(int $code, ?ClientExceptionInterface $exception = null): string { + $message = ''; + switch ($code) { + case 400: + if (method_exists($exception, 'getResponse') && !empty($exception->getResponse())) { + $responsebody = json_decode($exception->getResponse()->getBody()->getContents()); + if (property_exists($responsebody, 'error') && property_exists($responsebody->error, 'status') + && $responsebody->error->status === 'INVALID_ARGUMENT') { + $message = get_string('err_contentpolicyviolation', 'aitool_imagen'); + } + } + break; + } + return $message; + } +} diff --git a/tools/imagen/classes/instance.php b/tools/imagen/classes/instance.php new file mode 100644 index 0000000..d0c77a0 --- /dev/null +++ b/tools/imagen/classes/instance.php @@ -0,0 +1,58 @@ +. + +namespace aitool_imagen; + +use local_ai_manager\base_instance; +use local_ai_manager\local\aitool_option_vertexai; +use stdClass; + +/** + * Instance class for the connector instance of aitool_imagen. + * + * @package aitool_imagen + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class instance extends base_instance { + + #[\Override] + protected function extend_form_definition(\MoodleQuickForm $mform): void { + aitool_option_vertexai::extend_form_definition($mform); + // Condition is always true, but there does not seem to be an easy way to always hide an element. + $mform->hideIf('apikey', 'connector', 'imagen'); + } + + #[\Override] + protected function get_extended_formdata(): stdClass { + return aitool_option_vertexai::add_vertexai_to_form_data($this->get_customfield1()); + } + + #[\Override] + protected function extend_store_formdata(stdClass $data): void { + + [$serviceaccountjson, $baseendpoint] = aitool_option_vertexai::extract_vertexai_to_store($data); + + $this->set_customfield1($serviceaccountjson); + $this->set_endpoint($baseendpoint . ':predict'); + } + + #[\Override] + protected function extend_validation(array $data, array $files): array { + return aitool_option_vertexai::validate_vertexai($data); + } +} diff --git a/tools/imagen/classes/privacy/provider.php b/tools/imagen/classes/privacy/provider.php new file mode 100644 index 0000000..e0aee65 --- /dev/null +++ b/tools/imagen/classes/privacy/provider.php @@ -0,0 +1,38 @@ +. + +namespace aitool_imagen\privacy; + +/** + * aitool_imagen privacy provider class. + * + * @package aitool_imagen + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string the reason for storing no data + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/tools/imagen/lang/de/aitool_imagen.php b/tools/imagen/lang/de/aitool_imagen.php new file mode 100644 index 0000000..dde417b --- /dev/null +++ b/tools/imagen/lang/de/aitool_imagen.php @@ -0,0 +1,30 @@ +. + +/** + * Lang strings for aitool_imagen - DE. + * + * @package aitool_imagen + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['adddescription'] = 'Google Imagen ist ein KI-Modell von Google, das darauf spezialisiert ist, aus Textbeschreibungen Bilder zu generieren.'; +$string['err_contentpolicyviolation'] = 'Ihre Anfrage wurde vom Sicherheitssystem des KI-Tools zurückgewiesen. Ihr Prompt enthält vermutlich eine Anweisung, die nicht erlaubt ist.'; +$string['err_predictionmissing'] = 'Auf Ihre Anfrage hin konnte kein Bild generiert werden. Ihr Prompt enthält vermutlich eine Anweisung, die nicht erlaubt ist.'; +$string['pluginname'] = 'Google Imagen'; +$string['privacy:metadata'] = 'Das Subplugin "Google Imagen" des AI-Manager-Plugins speichert keine personenbezogenen Daten.'; diff --git a/tools/imagen/lang/en/aitool_imagen.php b/tools/imagen/lang/en/aitool_imagen.php new file mode 100644 index 0000000..168f231 --- /dev/null +++ b/tools/imagen/lang/en/aitool_imagen.php @@ -0,0 +1,30 @@ +. + +/** + * Lang strings for aitool_imagen - EN. + * + * @package aitool_imagen + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['adddescription'] = 'Google Imagen is an AI model by Google that specializes in generating images from text descriptions.'; +$string['err_contentpolicyviolation'] = 'Your request was rejected as a result of our safety system. Your prompt probably requests something that is not allowed.'; +$string['err_predictionmissing'] = 'No image could be generated based on your prompt. Your prompt probably requests something that is not allowed.'; +$string['pluginname'] = 'Google Imagen'; +$string['privacy:metadata'] = 'The local ai_manager tool subplugin "Google Imagen" does not store any personal data.'; diff --git a/tools/imagen/version.php b/tools/imagen/version.php new file mode 100644 index 0000000..fa41bbc --- /dev/null +++ b/tools/imagen/version.php @@ -0,0 +1,31 @@ +. + +/** + * Version file for aitool_imagen. + * + * @package aitool_imagen + * @copyright 2024 ISB Bayern + * @author Philipp Memmel + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2024120200; +$plugin->requires = 2023042403; +$plugin->release = '0.0.1'; +$plugin->component = 'aitool_imagen'; +$plugin->maturity = MATURITY_ALPHA; diff --git a/version.php b/version.php index de4c8d2..7cd9193 100644 --- a/version.php +++ b/version.php @@ -24,8 +24,8 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024092700; +$plugin->version = 2024120400; $plugin->requires = 2024042200; -$plugin->release = '0.0.2'; +$plugin->release = '0.0.3'; $plugin->component = 'local_ai_manager'; $plugin->maturity = MATURITY_ALPHA;