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
+ }
+}}
+