Skip to content

Commit

Permalink
Merge pull request #39 from mebis-lp/MBS-9809-AI_outside_courses
Browse files Browse the repository at this point in the history
MBS-9809: AI outside courses
  • Loading branch information
PhMemmel authored Jan 16, 2025
2 parents 2ad17cc + 0ca8a0e commit 12b849d
Show file tree
Hide file tree
Showing 25 changed files with 718 additions and 151 deletions.
2 changes: 1 addition & 1 deletion amd/build/make_request.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion amd/build/make_request.min.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 19 additions & 9 deletions amd/src/make_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,41 @@ import {call as fetchMany} from 'core/ajax';

/**
* Call to store input value
* @param {string} purpose
* @param {string} prompt
* @param {array} options
* @returns {mixed}
* @param {string} purpose the purpose to use for the request
* @param {string} prompt the prompt of the request
* @param {string} component the component from which the request is being done
* @param {number} contextid the id of the context from which the request is being done, or 0 for system context
* @param {string} options additional options, as json encoded string, or an empty string if no additional options
* @returns {Promise} the request promise
*/
const execMakeRequest = (
purpose,
prompt,
component,
contextid,
options
) => fetchMany([{
methodname: 'local_ai_manager_post_query',
args: {
purpose,
prompt,
component,
contextid,
options
},
}])[0];

/**
* Executes the call to store input value.
* @param {string} purpose
* @param {string} prompt
* @param {array} options
*
* @param {string} purpose the purpose to use for the request
* @param {string} prompt the prompt of the request
* @param {string} component the component from which the request is being done
* @param {number} contextid the id of the context from which the request is being done,
* will default to 0 (which means system context)
* @param {object} options additional options
* @returns {mixed}
*/
export const makeRequest = async(purpose, prompt, options = {}) => {
return execMakeRequest(purpose, prompt, JSON.stringify(options));
export const makeRequest = async(purpose, prompt, component, contextid = 0, options = {}) => {
return execMakeRequest(purpose, prompt, component, contextid, JSON.stringify(options));
};
21 changes: 21 additions & 0 deletions classes/ai_manager_utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

namespace local_ai_manager;

use context;
use local_ai_manager\local\tenant;
use local_ai_manager\local\userinfo;
use local_ai_manager\local\userusage;
Expand Down Expand Up @@ -207,4 +208,24 @@ public static function get_ai_config(stdClass $user, ?string $tenant = null): ar
'tools' => $tools,
];
}

/**
* Determines the closest course parent context based on the past context.
*
* Will return null if the context has no parent course context.
*
* @param context $context The context to find the closest parent course context for
* @return context|null The closest parent course context or null if there is not course parent context
*/
public static function find_closest_parent_course_context(context $context): ?context {
if ($context->contextlevel < CONTEXT_COURSE) {
// There can't be a course context in a context with contextlevel below course context,
// because these are CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT.
return null;
}
if ($context->contextlevel === CONTEXT_COURSE) {
return $context;
}
return self::find_closest_parent_course_context($context->get_parent_context());
}
}
15 changes: 9 additions & 6 deletions classes/base_connector.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,21 @@ protected function get_api_key(): string {
* Retrieves the data for the prompt based on the prompt text.
*
* @param string $prompttext the prompt text
* @param array $requestoptions the options of the request
* @param request_options $requestoptions the request_options object of the request
* @return array the prompt data
*/
abstract public function get_prompt_data(string $prompttext, array $requestoptions): array;
abstract public function get_prompt_data(string $prompttext, request_options $requestoptions): array;

/**
* Function to handle the result after the request has been made.
*
* This function extracts the data from the request result and puts it into a prompt_response object.
*
* @param StreamInterface $result the result of the request
* @param array $options the request options
* @param request_options $requestoptions the request options
* @return prompt_response the prompt response object containing the extracted data
*/
abstract public function execute_prompt_completion(StreamInterface $result, array $options = []): prompt_response;
abstract public function execute_prompt_completion(StreamInterface $result, request_options $requestoptions): prompt_response;

/**
* Defines if the connector uses the first customvalue attribute.
Expand Down Expand Up @@ -154,10 +154,13 @@ public function get_available_options(): array {
* if really necessary.
*
* @param array $data The data to send with the request.
* @return array The response from the request.
* @param request_options $requestoptions The request options of the request.
* Should only be necessary to be used in special cases (if for example there is being done another request inside this
* function, for example if a prompt is being translated before the "real" request is being called)
* @return request_response The response from the request.
* @throws \moodle_exception If the API key is empty.
*/
public function make_request(array $data): request_response {
public function make_request(array $data, request_options $requestoptions): request_response {
$client = new http_client([
'timeout' => get_config('local_ai_manager', 'requesttimeout'),
'verify' => !empty(get_config('local_ai_manager', 'verifyssl')),
Expand Down
43 changes: 25 additions & 18 deletions classes/external/submit_query.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ class submit_query extends external_api {
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'purpose' => new external_value(PARAM_TEXT, 'The purpose of the prompt.', VALUE_REQUIRED),
'prompt' => new external_value(PARAM_RAW, 'The prompt', VALUE_REQUIRED),
'options' => new external_value(PARAM_RAW, 'Options object JSON stringified', VALUE_DEFAULT, ''),
'purpose' => new external_value(PARAM_TEXT, 'The purpose of the prompt.', VALUE_REQUIRED),
'prompt' => new external_value(PARAM_RAW, 'The prompt', VALUE_REQUIRED),
'component' => new external_value(PARAM_ALPHANUMEXT, 'The component name', VALUE_REQUIRED),
'contextid' => new external_value(PARAM_INT, 'The context id of the context from which the request is being done',
VALUE_REQUIRED),
'options' => new external_value(PARAM_RAW, 'Options object JSON stringified', VALUE_DEFAULT, ''),
]);
}

Expand All @@ -52,27 +55,31 @@ public static function execute_parameters(): external_function_parameters {
* @param string $options additional options which should be passed to the request to the AI tool
* @return array associative array containing the result of the request
*/
public static function execute(string $purpose, string $prompt, string $options): array {
public static function execute(string $purpose, string $prompt, string $component, int $contextid, string $options): array {
[
'purpose' => $purpose,
'prompt' => $prompt,
'options' => $options,
'purpose' => $purpose,
'prompt' => $prompt,
'component' => $component,
'contextid' => $contextid,
'options' => $options,
] = self::validate_parameters(self::execute_parameters(), [
'purpose' => $purpose,
'prompt' => $prompt,
'options' => $options,
'purpose' => $purpose,
'prompt' => $prompt,
'component' => $component,
'contextid' => $contextid,
'options' => $options,
]);
if (!empty($options)) {
$options = json_decode($options, true);
}
$context = !empty($options['contextid']) ? \context::instance_by_id($options['contextid']) : \context_system::instance();
$context = $contextid === 0 ? \context_system::instance() : \context::instance_by_id($contextid);
self::validate_context($context);
// We do not check the 'local/ai_manager:use' capability here, because this is being done inside manager::perform_request.

try {
$aimanager = new \local_ai_manager\manager($purpose);

$result = $aimanager->perform_request($prompt, $options);
$result = $aimanager->perform_request($prompt, $component, $contextid, $options);

if ($result->get_code() !== 200) {
$error = ['message' => $result->get_errormessage()];
Expand All @@ -99,12 +106,12 @@ public static function execute(string $purpose, string $prompt, string $options)
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure(
[
'code' => new external_value(PARAM_INT, 'Return code of process.'),
'string' => new external_value(PARAM_TEXT, 'Return string of process.'),
'result' => new external_value(PARAM_RAW, 'The query result'),
],
'Result of a query'
[
'code' => new external_value(PARAM_INT, 'Return code of process.'),
'string' => new external_value(PARAM_TEXT, 'Return string of process.'),
'result' => new external_value(PARAM_RAW, 'The query result'),
],
'Result of a query'
);
}
}
12 changes: 12 additions & 0 deletions classes/form/rights_config_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class rights_config_form extends \moodleform {
/** @var string Constant for defining the action option "confirm" for the action {@see self::ACTION_CHANGE_CONFIRM_STATUS}. */
const ACTIONOPTION_CHANGE_CONFIRM_STATE_UNCONFIRM = 'unconfirm';

/** @var string Constant for defining the action "change usage scope". */
const ACTION_CHANGE_SCOPE = 'changescope';

/**
* Form definition.
*/
Expand All @@ -70,6 +73,7 @@ public function definition() {
self::ACTION_ASSIGN_ROLE => get_string('assignrole', 'local_ai_manager'),
self::ACTION_CHANGE_LOCK_STATE => get_string('changelockstate', 'local_ai_manager'),
self::ACTION_CHANGE_CONFIRM_STATE => get_string('changeconfirmstate', 'local_ai_manager'),
self::ACTION_CHANGE_SCOPE => get_string('changescope', 'local_ai_manager'),
]);

$actionselectsgroup[] = $mform->createElement('select', 'role', '', [
Expand All @@ -96,6 +100,14 @@ public function definition() {
);
$mform->hideif('confirmstate', 'action', 'neq', self::ACTION_CHANGE_CONFIRM_STATE);

$actionselectsgroup[] = $mform->createElement('select', 'scope', '',
[
userinfo::SCOPE_COURSES_ONLY => get_string('scope_courses', 'local_ai_manager'),
userinfo::SCOPE_EVERYWHERE => get_string('scope_everywhere', 'local_ai_manager'),
]
);
$mform->hideif('scope', 'action', 'neq', self::ACTION_CHANGE_SCOPE);

$mform->addGroup($actionselectsgroup, 'actiongroup', get_string('executebulkuseractions', 'local_ai_manager') . ':', [' '],
false);

Expand Down
32 changes: 28 additions & 4 deletions classes/local/rights_config_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function __construct(
$this->set_attribute('id', $uniqid);
$this->define_baseurl($baseurl);
// Define the list of columns to show.
$columns = ['checkbox', 'lastname', 'firstname', 'role', 'locked', 'confirmed'];
$columns = ['checkbox', 'lastname', 'firstname', 'role', 'locked', 'confirmed', 'scope'];
$checkboxheader = html_writer::div('', 'rights-table-selection_info', ['id' => 'rights-table-selection_info']);
$checkboxheader .= html_writer::empty_tag('input', ['type' => 'checkbox', 'id' => 'rights-table-selectall_checkbox']);
$headers = [
Expand All @@ -67,6 +67,7 @@ public function __construct(
get_string('role', 'local_ai_manager'),
get_string('locked', 'local_ai_manager'),
get_string('confirmed', 'local_ai_manager'),
get_string('scope', 'local_ai_manager'),
];

$tenantfield = get_config('local_ai_manager', 'tenantcolumn');
Expand Down Expand Up @@ -100,7 +101,7 @@ public function __construct(
}
}

$fields = 'u.id as id, lastname, firstname, role, locked, ui.confirmed';
$fields = 'u.id as id, lastname, firstname, role, locked, ui.confirmed, ui.scope';
$from =
'{user} u LEFT JOIN {local_ai_manager_userinfo} ui ON u.id = ui.userid';
$where = 'u.deleted != 1 AND u.suspended != 1 AND ' . $tenantfield . ' = :tenant' . $rolewhere;
Expand All @@ -117,16 +118,17 @@ public function __construct(
$this->no_sorting('role');
$this->no_sorting('locked');
$this->no_sorting('confirmed');
$this->no_sorting('scope');
$this->collapsible(false);
$this->sortable(true, 'lastname');

$this->set_sql($usertableextend->get_fields(), $usertableextend->get_from(),
$usertableextend->get_where() . ' GROUP BY u.id, role, locked, ui.confirmed',
$usertableextend->get_where() . ' GROUP BY u.id, role, locked, ui.confirmed, ui.scope',
$usertableextend->get_params());
// We need to use this because we are using "GROUP BY" which is not being expected by the sql table.
$this->set_count_sql("SELECT COUNT(*) FROM (SELECT " . $usertableextend->get_fields() . " FROM "
. $usertableextend->get_from() . " WHERE " . $usertableextend->get_where() .
" GROUP BY u.id, role, locked, ui.confirmed) AS subquery",
" GROUP BY u.id, role, locked, ui.confirmed, ui.scope) AS subquery",
$usertableextend->get_params());
parent::setup();
}
Expand Down Expand Up @@ -174,6 +176,28 @@ public function col_confirmed($value) {
}
}

/**
* Get the icon representing the user scope.
*
* @param stdClass $value the object containing the information of the current row
* @return string the resulting string for the confirmed column
*/
public function col_scope($value) {
$userinfo = new userinfo($value->id);
$scope = empty($value->scope) ? $userinfo->get_default_scope() : intval($value->scope);
switch ($scope) {
case userinfo::SCOPE_EVERYWHERE:
return '<i class="fa fa-globe local_ai_manager-green" title="' .
get_string('scope_everywhere', 'local_ai_manager') . '"></i>';
case userinfo::SCOPE_COURSES_ONLY:
return '<i class="fa fa-graduation-cap local_ai_manager-red" title="' .
get_string('scope_courses', 'local_ai_manager') . '"></i>';
default:
// Should not happen.
return 'No scope';
}
}

#[\Override]
public function other_cols($column, $row) {
if ($column === 'checkbox') {
Expand Down
Loading

0 comments on commit 12b849d

Please sign in to comment.