diff --git a/ai/classes/aiactions/base.php b/ai/classes/aiactions/base.php index c59ce918e5b..3a764df9ac1 100644 --- a/ai/classes/aiactions/base.php +++ b/ai/classes/aiactions/base.php @@ -89,8 +89,8 @@ abstract class base { * * @return string The system instruction for the action. */ - public function get_system_instruction(): string { - $stringid = 'action_' . $this->get_basename() . '_instruction'; + public static function get_system_instruction(): string { + $stringid = 'action_' . self::get_basename() . '_instruction'; // If the string doesn't exist, return an empty string. if (!get_string_manager()->string_exists($stringid, 'core_ai')) { diff --git a/ai/classes/provider.php b/ai/classes/provider.php index b2b1054848a..1dc1afc5d61 100644 --- a/ai/classes/provider.php +++ b/ai/classes/provider.php @@ -54,4 +54,21 @@ abstract class provider { return get_string('pluginname', $component); } + /** + * Get any action settings for this provider. + * + * @param string $action The action class name. + * @param \admin_root $ADMIN The admin root object. + * @param string $section The section name. + * @param bool $hassiteconfig Whether the current user has moodle/site:config capability. + * @return array An array of settings. + */ + public function get_action_settings( + string $action, + \admin_root $ADMIN, + string $section, + bool $hassiteconfig + ): array { + return []; + } } diff --git a/ai/classes/table/aiprovider_action_management_table.php b/ai/classes/table/aiprovider_action_management_table.php index 38e5473c33d..51dd3d28790 100644 --- a/ai/classes/table/aiprovider_action_management_table.php +++ b/ai/classes/table/aiprovider_action_management_table.php @@ -175,7 +175,14 @@ class aiprovider_action_management_table extends flexible_table implements dynam * @return string */ protected function col_settings(stdClass $row): string { - // TODO: MDL-82609 - Add settings link. + global $CFG; + require_once($CFG->libdir . '/adminlib.php'); // Needed for the AJAX calls. + $tree = \admin_get_root(); + $sectionname = $this->pluginname . '_' . $row->action::get_basename(); + $section = $tree->locate($sectionname); + if ($section) { + return \html_writer::link($section->get_settings_page_url(), get_string('settings')); + } return ''; } diff --git a/ai/provider/openai/classes/abstract_processor.php b/ai/provider/openai/classes/abstract_processor.php index e7f2a697841..7f2839190c6 100644 --- a/ai/provider/openai/classes/abstract_processor.php +++ b/ai/provider/openai/classes/abstract_processor.php @@ -47,6 +47,15 @@ abstract class abstract_processor extends process_base { */ abstract protected function get_model(): string; + /** + * Get the system instructions. + * + * @return string + */ + protected function get_system_instruction(): string { + return $this->action::get_system_instruction(); + } + /** * Create the request object to send to the OpenAI API. * diff --git a/ai/provider/openai/classes/process_generate_image.php b/ai/provider/openai/classes/process_generate_image.php index 94ffc204ee3..d43da6048f7 100644 --- a/ai/provider/openai/classes/process_generate_image.php +++ b/ai/provider/openai/classes/process_generate_image.php @@ -22,6 +22,7 @@ use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriInterface; /** * Class process image generation. @@ -38,13 +39,13 @@ class process_generate_image extends abstract_processor { private string $responseformat = 'url'; #[\Override] - protected function get_endpoint(): \Psr\Http\Message\UriInterface { - return new Uri('https://api.openai.com/v1/images/generations'); + protected function get_endpoint(): UriInterface { + return new Uri(get_config('aiprovider_openai', 'action_generate_image_endpoint')); } #[\Override] protected function get_model(): string { - return 'dall-e-3'; + return get_config('aiprovider_openai', 'action_generate_image_model'); } #[\Override] diff --git a/ai/provider/openai/classes/process_generate_text.php b/ai/provider/openai/classes/process_generate_text.php index 592662b5505..eed7be703e2 100644 --- a/ai/provider/openai/classes/process_generate_text.php +++ b/ai/provider/openai/classes/process_generate_text.php @@ -32,12 +32,17 @@ use Psr\Http\Message\UriInterface; class process_generate_text extends abstract_processor { #[\Override] protected function get_endpoint(): UriInterface { - return new Uri('https://api.openai.com/v1/chat/completions'); + return new Uri(get_config('aiprovider_openai', 'action_generate_text_endpoint')); } #[\Override] protected function get_model(): string { - return 'gpt-4o'; + return get_config('aiprovider_openai', 'action_generate_text_model'); + } + + #[\Override] + protected function get_system_instruction(): string { + return get_config('aiprovider_openai', 'action_generate_text_systeminstruction'); } #[\Override] @@ -53,7 +58,7 @@ class process_generate_text extends abstract_processor { $requestobj->user = $userid; // If there is a system string available, use it. - $systeminstruction = $this->action->get_system_instruction(); + $systeminstruction = $this->get_system_instruction(); if (!empty($systeminstruction)) { $systemobj = new \stdClass(); $systemobj->role = 'system'; diff --git a/ai/provider/openai/classes/process_summarise_text.php b/ai/provider/openai/classes/process_summarise_text.php index cf36f53c621..c16530f11e9 100644 --- a/ai/provider/openai/classes/process_summarise_text.php +++ b/ai/provider/openai/classes/process_summarise_text.php @@ -16,6 +16,9 @@ namespace aiprovider_openai; +use GuzzleHttp\Psr7\Uri; +use Psr\Http\Message\UriInterface; + /** * Class process text summarisation. * @@ -24,4 +27,18 @@ namespace aiprovider_openai; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_summarise_text extends process_generate_text { + #[\Override] + protected function get_endpoint(): UriInterface { + return new Uri(get_config('aiprovider_openai', 'action_summarise_text_endpoint')); + } + + #[\Override] + protected function get_model(): string { + return get_config('aiprovider_openai', 'action_summarise_text_model'); + } + + #[\Override] + protected function get_system_instruction(): string { + return get_config('aiprovider_openai', 'action_summarise_text_systeminstruction'); + } } diff --git a/ai/provider/openai/classes/provider.php b/ai/provider/openai/classes/provider.php index 29795dd45e4..5a7c0f89025 100644 --- a/ai/provider/openai/classes/provider.php +++ b/ai/provider/openai/classes/provider.php @@ -143,4 +143,68 @@ class provider extends \core_ai\provider { return true; } + + /** + * Get any action settings for this provider. + * + * @param string $action The action class name. + * @param \admin_root $ADMIN The admin root object. + * @param string $section The section name. + * @param bool $hassiteconfig Whether the current user has moodle/site:config capability. + * @return array An array of settings. + */ + public function get_action_settings( + string $action, + \admin_root $ADMIN, + string $section, + bool $hassiteconfig + ): array { + $actionname = substr($action, (strrpos($action, '\\') + 1)); + $settings = []; + if ($actionname === 'generate_text' || $actionname === 'summarise_text') { + // Add the model setting. + $settings[] = new \admin_setting_configtext( + "aiprovider_openai/action_{$actionname}_model", + new \lang_string("action:{$actionname}:model", 'aiprovider_openai'), + new \lang_string("action:{$actionname}:model_desc", 'aiprovider_openai'), + 'gpt-4o', + PARAM_TEXT, + ); + // Add API endpoint. + $settings[] = new \admin_setting_configtext( + "aiprovider_openai/action_{$actionname}_endpoint", + new \lang_string("action:{$actionname}:endpoint", 'aiprovider_openai'), + new \lang_string("action:{$actionname}:endpoint_desc", 'aiprovider_openai'), + 'https://api.openai.com/v1/chat/completions', + PARAM_URL, + ); + // Add system instruction settings. + $settings[] = new \admin_setting_configtextarea( + "aiprovider_openai/action_{$actionname}_systeminstruction", + new \lang_string("action:{$actionname}:systeminstruction", 'aiprovider_openai'), + new \lang_string("action:{$actionname}:systeminstruction_desc", 'aiprovider_openai'), + $action::get_system_instruction(), + PARAM_TEXT + ); + } else if ($actionname === 'generate_image') { + // Add the model setting. + $settings[] = new \admin_setting_configtext( + "aiprovider_openai/action_{$actionname}_model", + new \lang_string("action:{$actionname}:model", 'aiprovider_openai'), + new \lang_string("action:{$actionname}:model_desc", 'aiprovider_openai'), + 'dall-e-3', + PARAM_TEXT, + ); + // Add API endpoint. + $settings[] = new \admin_setting_configtext( + "aiprovider_openai/action_{$actionname}_endpoint", + new \lang_string("action:{$actionname}:endpoint", 'aiprovider_openai'), + new \lang_string("action:{$actionname}:endpoint_desc", 'aiprovider_openai'), + 'https://api.openai.com/v1/images/generations', + PARAM_URL, + ); + } + + return $settings; + } } diff --git a/ai/provider/openai/lang/en/aiprovider_openai.php b/ai/provider/openai/lang/en/aiprovider_openai.php index e935248f44f..9e4937003de 100644 --- a/ai/provider/openai/lang/en/aiprovider_openai.php +++ b/ai/provider/openai/lang/en/aiprovider_openai.php @@ -22,6 +22,22 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['action:generate_image:endpoint'] = 'API endpoint'; +$string['action:generate_image:endpoint_desc'] = 'The API endpoint the provider uses for this action.'; +$string['action:generate_image:model'] = 'Image generation model'; +$string['action:generate_image:model_desc'] = 'The model used to generate images from user prompts.'; +$string['action:generate_text:endpoint'] = 'API endpoint'; +$string['action:generate_text:endpoint_desc'] = 'The API endpoint for the provider uses for this action.'; +$string['action:generate_text:model'] = 'Text generation model'; +$string['action:generate_text:model_desc'] = 'The model used to generate the text response.'; +$string['action:generate_text:systeminstruction'] = 'System instruction'; +$string['action:generate_text:systeminstruction_desc'] = 'This instruction is provided together with the user prompt for this action. It provides information to the AI model on how to generate the response.'; +$string['action:summarise_text:endpoint'] = 'API endpoint'; +$string['action:summarise_text:endpoint_desc'] = 'The API endpoint the provider uses for this action.'; +$string['action:summarise_text:model'] = 'Text summarisation model'; +$string['action:summarise_text:model_desc'] = 'The model used to summarise the provided text.'; +$string['action:summarise_text:systeminstruction'] = 'System instruction'; +$string['action:summarise_text:systeminstruction_desc'] = 'This instruction is provided together with the user prompt for this action. It provides information to the AI model on how to generate the response.'; $string['apikey'] = 'OpenAI API key'; $string['apikey_desc'] = 'Enter your OpenAI API key. You can get one from here'; $string['enableglobalratelimit'] = 'Enable global rate limiting'; diff --git a/ai/provider/openai/tests/process_generate_text_test.php b/ai/provider/openai/tests/process_generate_text_test.php index bcacf60c6ee..910683fd77c 100644 --- a/ai/provider/openai/tests/process_generate_text_test.php +++ b/ai/provider/openai/tests/process_generate_text_test.php @@ -83,8 +83,8 @@ final class process_generate_text_test extends \advanced_testcase { $body = (object) json_decode($request->getBody()->getContents()); - $this->assertEquals('This is a test prompt', $body->messages[0]->content); - $this->assertEquals('user', $body->messages[0]->role); + $this->assertEquals('This is a test prompt', $body->messages[1]->content); + $this->assertEquals('user', $body->messages[1]->role); } /** diff --git a/ai/provider/openai/version.php b/ai/provider/openai/version.php index 4722e9e0870..3d8e25da439 100644 --- a/ai/provider/openai/version.php +++ b/ai/provider/openai/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'aiprovider_openai'; -$plugin->version = 2024060900; +$plugin->version = 2024091300; $plugin->requires = 2024041600; $plugin->maturity = MATURITY_STABLE; diff --git a/lang/en/ai.php b/lang/en/ai.php index 7089d37eb21..56328c2625c 100644 --- a/lang/en/ai.php +++ b/lang/en/ai.php @@ -27,6 +27,9 @@ $string['action_generate_image'] = 'Generate image'; $string['action_generate_image_desc'] = 'Generates an image based on a text prompt.'; $string['action_generate_text'] = 'Generate text'; $string['action_generate_text_desc'] = 'Generates text based on a text prompt.'; +$string['action_generate_text_instruction'] = 'You will receive a text input from the user. Your task is to generate text based on their request. Follow these important instructions: + 1. Return the summary in plain text only. + 2. Do not include any markdown formatting, greetings, or platitudes.'; $string['action_summarise_text'] = 'Summarise text'; $string['action_summarise_text_desc'] = 'Summarises text based on provided input text.'; $string['action_summarise_text_instruction'] = 'You will receive a text input from the user. Your task is to summarize the provided text. Follow these guidelines: @@ -41,6 +44,9 @@ Important Instructions: Ensure the summary is easy to read and effectively conveys the main points of the original text.'; $string['action_translate_text'] = 'Translate text'; $string['action_translate_text_desc'] = 'Translate provided text from one language to another.'; +$string['actionsettingprovider'] = '{$a} action settings'; +$string['actionsettingprovider_desc'] = 'These settings are specifc to this action for this provider.
+They are used to control how the action is processed by the provider.'; $string['ai'] = 'AI'; $string['aiplacementsettings'] = 'Manage settings for AI placements'; $string['aiprovidersettings'] = 'Manage settings for AI providers'; diff --git a/lib/classes/plugininfo/aiprovider.php b/lib/classes/plugininfo/aiprovider.php index 01bf9e9b43c..1f443358b67 100644 --- a/lib/classes/plugininfo/aiprovider.php +++ b/lib/classes/plugininfo/aiprovider.php @@ -18,7 +18,6 @@ namespace core\plugininfo; use core_plugin_manager; use moodle_url; -use core\lang_string; /** * AI placement plugin info class. @@ -103,18 +102,36 @@ class aiprovider extends base { } else { // Provider action settings heading. $settings->add(new \admin_setting_heading("{$section}/generals", - new lang_string('provideractionsettings', 'core_ai'), - new lang_string('provideractionsettings_desc', 'core_ai'))); + new \lang_string('provideractionsettings', 'core_ai'), + new \lang_string('provideractionsettings_desc', 'core_ai'))); // Load the setting table of actions that this provider supports. $settings->add(new \core_ai\admin\admin_setting_action_manager( $section, \core_ai\table\aiprovider_action_management_table::class, 'manageaiproviders', - new lang_string('manageaiproviders', 'core_ai'), + new \lang_string('manageaiproviders', 'core_ai'), )); } - $ADMIN->add($parentnodename, $settings); + // Load any action settings for this provider. + $providerclass = "\\{$section}\\provider"; + $provider = new $providerclass(); + $actionlist = $provider->get_action_list(); + foreach ($actionlist as $action) { + $actionsettings = $provider->get_action_settings($action, $ADMIN, $section, $hassiteconfig); + if (!empty($actionsettings)) { + $actionname = substr($action, (strrpos($action, '\\') + 1)); + $settings = new \admin_settingpage($section . '_' . $actionname, $action::get_name(), 'moodle/site:config', true); + $setting = new \admin_setting_heading("{$section}_actions/heading", + new \lang_string('actionsettingprovider', 'core_ai', $provider->get_name()), + new \lang_string('actionsettingprovider_desc', 'core_ai')); + $settings->add($setting); + foreach ($actionsettings as $setting) { + $settings->add($setting); + } + $ADMIN->add('root', $settings); + } + } } /**