Merge branch '4539' into 1.11.x

pull/4550/head
Yannick Warnier 3 years ago
commit 4b29d6d8bc
  1. 8
      main/exercise/aiken.php
  2. 543
      main/exercise/export/aiken/aiken_import.inc.php
  3. 1
      main/inc/lib/plugin.lib.php
  4. 9
      main/lang/english/trad4all.inc.php
  5. 8
      main/lang/french/trad4all.inc.php
  6. 8
      main/lang/spanish/trad4all.inc.php
  7. 83
      plugin/ai_helper/AiHelperPlugin.php
  8. 13
      plugin/ai_helper/README.md
  9. 16
      plugin/ai_helper/install.php
  10. 13
      plugin/ai_helper/lang/english.php
  11. 13
      plugin/ai_helper/lang/french.php
  12. 13
      plugin/ai_helper/lang/spanish.php
  13. 6
      plugin/ai_helper/plugin.php
  14. 332
      plugin/ai_helper/src/openai/OpenAi.php
  15. 74
      plugin/ai_helper/src/openai/Url.php
  16. 68
      plugin/ai_helper/tool/answers.php
  17. 16
      plugin/ai_helper/uninstall.php

@ -40,8 +40,16 @@ if ((api_is_allowed_to_edit(null, true))) {
exit;
}
}
if (isset($_REQUEST['submit_aiken_generated'])) {
$id = aikenImportExercise(null, $_REQUEST);
if (is_numeric($id) && !empty($id)) {
header('Location: admin.php?'.api_get_cidreq().'&exerciseId='.$id);
exit;
}
}
}
Display::display_header(get_lang('ImportAikenQuiz'), 'Exercises');
aiken_display_form();
generateAikenForm();
Display::display_footer();

@ -43,6 +43,106 @@ function aiken_display_form()
echo $form;
}
/**
* Generates aiken format using AI api.
* Requires plugin ai_helper to connect to the api.
*/
function generateAikenForm()
{
if ('true' !== api_get_plugin_setting('ai_helper', 'tool_enable')) {
return false;
}
$form = new FormValidator(
'aiken_generate',
'post',
api_get_self()."?".api_get_cidreq(),
null,
);
$form->addElement('header', get_lang('AIQuestionsGenerator'));
$form->addElement('text', 'quiz_name', [get_lang('QuestionsTopic'),get_lang('QuestionsTopicHelp')]);
$form->addRule('quiz_name', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('number', 'nro_questions', [get_lang('NumberOfQuestions'), get_lang('AIQuestionsGeneratorNumberHelper')]);
$form->addRule('nro_questions', get_lang('ThisFieldIsRequired'), 'required');
$options = [
'multiple_choice' => get_lang('MultipleAnswer'),
];
$form->addElement(
'select',
'question_type',
get_lang('QuestionType'),
$options
);
$generateUrl = api_get_path(WEB_PLUGIN_PATH).'ai_helper/tool/answers.php';
$language = api_get_interface_language();
$form->addHtml('<script>
$(function () {
$("#aiken-area").hide();
$("#generate-aiken").on("click", function (e) {
e.preventDefault();
e.stopPropagation();
var btnGenerate = $(this);
var quizName = $("[name=\'quiz_name\']").val();
var nroQ = parseInt($("[name=\'nro_questions\']").val());
var qType = $("[name=\'question_type\']").val();
var valid = (quizName != \'\' && nroQ > 0);
if (valid) {
btnGenerate.attr("disabled", true);
btnGenerate.text("'.get_lang('PleaseWaitThisCouldTakeAWhile').'");
$("#textarea-aiken").text("");
$("#aiken-area").hide();
$.getJSON("'.$generateUrl.'", {
"quiz_name": quizName,
"nro_questions": nroQ,
"question_type": qType,
"language": "'.$language.'"
}).done(function (data) {
btnGenerate.attr("disabled", false);
btnGenerate.text("'.get_lang('Generate').'");
if (data.success && data.success == true) {
$("#aiken-area").show();
$("#textarea-aiken").text(data.text);
$("#textarea-aiken").focus();
} else {
alert("'.get_lang('NoSearchResults').'. '.get_lang('PleaseTryAgain').'");
}
});
}
});
});
</script>');
$form->addButton(
'generate_aiken_button',
get_lang('Generate'),
'',
'default',
'default',
null,
['id' => 'generate-aiken']
);
$form->addHtml('<div id="aiken-area">');
$form->addElement(
'textarea',
'aiken_format',
get_lang('Answers'),
[
'id' => 'textarea-aiken',
'style' => 'width: 100%; height: 250px;',
]
);
$form->addElement('number', 'total_weight', get_lang('TotalWeight'));
$form->addButtonImport(get_lang('Import'), 'submit_aiken_generated');
$form->addHtml('</div>');
echo $form->returnForm();
}
/**
* Gets the uploaded file (from $_FILES) and unzip it to the given directory.
*
@ -108,208 +208,201 @@ function get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)
* Main function to import the Aiken exercise.
*
* @param string $file
* @param array $request
*
* @return mixed True on success, error message on failure
*/
function aiken_import_exercise($file)
function aikenImportExercise($file = null, $request = [])
{
$archive_path = api_get_path(SYS_ARCHIVE_PATH).'aiken/';
$baseWorkDir = $archive_path;
$exerciseInfo = [];
$uploadPath = 'aiken_'.api_get_unique_id();
if (!is_dir($baseWorkDir.$uploadPath)) {
mkdir($baseWorkDir.$uploadPath, api_get_permissions_for_new_directories(), true);
}
if (isset($file)) {
// The import is from aiken file format.
$archivePath = api_get_path(SYS_ARCHIVE_PATH).'aiken/';
$baseWorkDir = $archivePath;
// set some default values for the new exercise
$exercise_info = [];
$exercise_info['name'] = preg_replace('/.(zip|txt)$/i', '', $file);
$exercise_info['question'] = [];
$uploadPath = 'aiken_'.api_get_unique_id();
if (!is_dir($baseWorkDir.$uploadPath)) {
mkdir($baseWorkDir.$uploadPath, api_get_permissions_for_new_directories(), true);
}
// if file is not a .zip, then we cancel all
if (!preg_match('/.(zip|txt)$/i', $file)) {
return 'YouMustUploadAZipOrTxtFile';
}
// set some default values for the new exercise
$exerciseInfo['name'] = preg_replace('/.(zip|txt)$/i', '', $file);
$exerciseInfo['question'] = [];
// unzip the uploaded file in a tmp directory
if (preg_match('/.(zip|txt)$/i', $file)) {
if (!get_and_unzip_uploaded_exercise($baseWorkDir.$uploadPath, '/')) {
return 'ThereWasAProblemWithYourFile';
// if file is not a .zip, then we cancel all
if (!preg_match('/.(zip|txt)$/i', $file)) {
return 'YouMustUploadAZipOrTxtFile';
}
// unzip the uploaded file in a tmp directory
if (preg_match('/.(zip|txt)$/i', $file)) {
if (!get_and_unzip_uploaded_exercise($baseWorkDir.$uploadPath, '/')) {
return 'ThereWasAProblemWithYourFile';
}
}
}
// find the different manifests for each question and parse them
$exerciseHandle = opendir($baseWorkDir.$uploadPath);
$file_found = false;
$operation = false;
$result = false;
// Parse every subdirectory to search txt question files
while (false !== ($file = readdir($exerciseHandle))) {
if (is_dir($baseWorkDir.'/'.$uploadPath.$file) && $file != "." && $file != "..") {
//find each manifest for each question repository found
$questionHandle = opendir($baseWorkDir.'/'.$uploadPath.$file);
while (false !== ($questionFile = readdir($questionHandle))) {
if (preg_match('/.txt$/i', $questionFile)) {
$result = aiken_parse_file(
$exercise_info,
$baseWorkDir,
$file,
$questionFile
);
$file_found = true;
// find the different manifests for each question and parse them
$exerciseHandle = opendir($baseWorkDir.$uploadPath);
$fileFound = false;
$operation = false;
$result = false;
// Parse every subdirectory to search txt question files
while (false !== ($file = readdir($exerciseHandle))) {
if (is_dir($baseWorkDir.'/'.$uploadPath.$file) && $file != "." && $file != "..") {
//find each manifest for each question repository found
$questionHandle = opendir($baseWorkDir.'/'.$uploadPath.$file);
while (false !== ($questionFile = readdir($questionHandle))) {
if (preg_match('/.txt$/i', $questionFile)) {
$result = aiken_parse_file(
$exerciseInfo,
$baseWorkDir,
$file,
$questionFile
);
$fileFound = true;
}
}
} elseif (preg_match('/.txt$/i', $file)) {
$result = aiken_parse_file($exerciseInfo, $baseWorkDir.$uploadPath, '', $file);
$fileFound = true;
}
} elseif (preg_match('/.txt$/i', $file)) {
$result = aiken_parse_file($exercise_info, $baseWorkDir.$uploadPath, '', $file);
$file_found = true;
}
}
if (!$file_found) {
$result = 'NoTxtFileFoundInTheZip';
}
if (!$fileFound) {
$result = 'NoTxtFileFoundInTheZip';
}
if (true !== $result) {
return $result;
if (true !== $result) {
return $result;
}
} elseif (!empty($request)) {
// The import is from aiken generated in textarea.
$exerciseInfo['name'] = $request['quiz_name'];
$exerciseInfo['question'] = [];
setExerciseInfoFromAikenText($request['aiken_format'], $exerciseInfo);
}
// 1. Create exercise.
$exercise = new Exercise();
$exercise->exercise = $exercise_info['name'];
$exercise->save();
$last_exercise_id = $exercise->selectId();
$tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
$tableAnswer = Database::get_course_table(TABLE_QUIZ_ANSWER);
if (!empty($last_exercise_id)) {
$courseId = api_get_course_int_id();
foreach ($exercise_info['question'] as $key => $question_array) {
// 2. Create question.
$question = new Aiken2Question();
$question->type = $question_array['type'];
$question->setAnswer();
$question->updateTitle($question_array['title']);
if (isset($question_array['description'])) {
$question->updateDescription($question_array['description']);
}
$type = $question->selectType();
$question->type = constant($type);
$question->save($exercise);
$last_question_id = $question->selectId();
// 3. Create answer
$answer = new Answer($last_question_id, $courseId, $exercise, false);
$answer->new_nbrAnswers = count($question_array['answer']);
$max_score = 0;
$scoreFromFile = 0;
if (isset($question_array['score']) && !empty($question_array['score'])) {
$scoreFromFile = $question_array['score'];
}
if (!empty($exerciseInfo)) {
$exercise = new Exercise();
$exercise->exercise = $exerciseInfo['name'];
$exercise->save();
$lastExerciseId = $exercise->selectId();
$tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
$tableAnswer = Database::get_course_table(TABLE_QUIZ_ANSWER);
if (!empty($lastExerciseId)) {
$courseId = api_get_course_int_id();
foreach ($exerciseInfo['question'] as $key => $questionArray) {
// 2. Create question.
$question = new Aiken2Question();
$question->type = $questionArray['type'];
$question->setAnswer();
$question->updateTitle($questionArray['title']);
if (isset($questionArray['description'])) {
$question->updateDescription($questionArray['description']);
}
$type = $question->selectType();
$question->type = constant($type);
$question->save($exercise);
$last_question_id = $question->selectId();
// 3. Create answer
$answer = new Answer($last_question_id, $courseId, $exercise, false);
$answer->new_nbrAnswers = count($questionArray['answer']);
$max_score = 0;
$scoreFromFile = 0;
if (isset($questionArray['score']) && !empty($questionArray['score'])) {
$scoreFromFile = $questionArray['score'];
}
foreach ($question_array['answer'] as $key => $answers) {
$key++;
$answer->new_answer[$key] = $answers['value'];
$answer->new_position[$key] = $key;
$answer->new_comment[$key] = '';
// Correct answers ...
if (isset($question_array['correct_answers']) &&
in_array($key, $question_array['correct_answers'])
) {
$answer->new_correct[$key] = 1;
if (isset($question_array['feedback'])) {
$answer->new_comment[$key] = $question_array['feedback'];
foreach ($questionArray['answer'] as $key => $answers) {
$key++;
$answer->new_answer[$key] = $answers['value'];
$answer->new_position[$key] = $key;
$answer->new_comment[$key] = '';
// Correct answers ...
if (isset($questionArray['correct_answers']) &&
in_array($key, $questionArray['correct_answers'])
) {
$answer->new_correct[$key] = 1;
if (isset($questionArray['feedback'])) {
$answer->new_comment[$key] = $questionArray['feedback'];
}
} else {
$answer->new_correct[$key] = 0;
}
} else {
$answer->new_correct[$key] = 0;
}
if (isset($question_array['weighting'][$key - 1])) {
$answer->new_weighting[$key] = $question_array['weighting'][$key - 1];
$max_score += $question_array['weighting'][$key - 1];
}
if (isset($questionArray['weighting'][$key - 1])) {
$answer->new_weighting[$key] = $questionArray['weighting'][$key - 1];
$max_score += $questionArray['weighting'][$key - 1];
}
if (!empty($scoreFromFile) && $answer->new_correct[$key]) {
$answer->new_weighting[$key] = $scoreFromFile;
}
if (!empty($scoreFromFile) && $answer->new_correct[$key]) {
$answer->new_weighting[$key] = $scoreFromFile;
}
$params = [
'c_id' => $courseId,
'question_id' => $last_question_id,
'answer' => $answer->new_answer[$key],
'correct' => $answer->new_correct[$key],
'comment' => $answer->new_comment[$key],
'ponderation' => isset($answer->new_weighting[$key]) ? $answer->new_weighting[$key] : '',
'position' => $answer->new_position[$key],
'hotspot_coordinates' => '',
'hotspot_type' => '',
];
$answerId = Database::insert($tableAnswer, $params);
if ($answerId) {
$params = [
'id_auto' => $answerId,
'iid' => $answerId,
'c_id' => $courseId,
'question_id' => $last_question_id,
'answer' => $answer->new_answer[$key],
'correct' => $answer->new_correct[$key],
'comment' => $answer->new_comment[$key],
'ponderation' => isset($answer->new_weighting[$key]) ? $answer->new_weighting[$key] : '',
'position' => $answer->new_position[$key],
'hotspot_coordinates' => '',
'hotspot_type' => '',
];
Database::update($tableAnswer, $params, ['iid = ?' => [$answerId]]);
$answerId = Database::insert($tableAnswer, $params);
if ($answerId) {
$params = [
'id_auto' => $answerId,
'iid' => $answerId,
];
Database::update($tableAnswer, $params, ['iid = ?' => [$answerId]]);
}
}
}
if (!empty($scoreFromFile)) {
$max_score = $scoreFromFile;
if (!empty($scoreFromFile)) {
$max_score = $scoreFromFile;
}
$params = ['ponderation' => $max_score];
Database::update(
$tableQuestion,
$params,
['iid = ?' => [$last_question_id]]
);
}
$params = ['ponderation' => $max_score];
Database::update(
$tableQuestion,
$params,
['iid = ?' => [$last_question_id]]
);
}
// Delete the temp dir where the exercise was unzipped
my_delete($baseWorkDir.$uploadPath);
$operation = $last_exercise_id;
// Delete the temp dir where the exercise was unzipped
my_delete($baseWorkDir.$uploadPath);
return $lastExerciseId;
}
}
return $operation;
return false;
}
/**
* Parses an Aiken file and builds an array of exercise + questions to be
* imported by the import_exercise() function.
*
* @param array The reference to the array in which to store the questions
* @param string Path to the directory with the file to be parsed (without final /)
* @param string Name of the last directory part for the file (without /)
* @param string Name of the file to be parsed (including extension)
* @param string $exercisePath
* @param string $file
* @param string $questionFile
*
* @return string|bool True on success, error message on error
* @assert ('','','') === false
* Set the exercise information from an aiken text formatted.
*/
function aiken_parse_file(&$exercise_info, $exercisePath, $file, $questionFile)
function setExerciseInfoFromAikenText($aikenText, &$exerciseInfo)
{
$questionTempDir = $exercisePath.'/'.$file.'/';
$questionFilePath = $questionTempDir.$questionFile;
if (!is_file($questionFilePath)) {
return 'FileNotFound';
}
$text = file_get_contents($questionFilePath);
$detect = mb_detect_encoding($text, 'ASCII', true);
$detect = mb_detect_encoding($aikenText, 'ASCII', true);
if ('ASCII' === $detect) {
$data = explode("\n", $text);
$data = explode("\n", $aikenText);
} else {
$text = str_ireplace(["\x0D", "\r\n"], "\n", $text); // Removes ^M char from win files.
$text = str_ireplace(["\x0D", "\r\n"], "\n", $aikenText); // Removes ^M char from win files.
$data = explode("\n\n", $text);
}
$question_index = 0;
$answers_array = [];
$questionIndex = 0;
$answersArray = [];
foreach ($data as $line => $info) {
$info = trim($info);
if (empty($info)) {
@ -320,117 +413,121 @@ function aiken_parse_file(&$exercise_info, $exercisePath, $file, $questionFile)
if (!mb_check_encoding($info, 'utf-8') && mb_check_encoding($info, 'iso-8859-1')) {
$info = utf8_encode($info);
}
$exercise_info['question'][$question_index]['type'] = 'MCUA';
$exerciseInfo['question'][$questionIndex]['type'] = 'MCUA';
if (preg_match('/^([A-Za-z])(\)|\.)\s(.*)/', $info, $matches)) {
//adding one of the possible answers
$exercise_info['question'][$question_index]['answer'][]['value'] = $matches[3];
$answers_array[] = $matches[1];
$exerciseInfo['question'][$questionIndex]['answer'][]['value'] = $matches[3];
$answersArray[] = $matches[1];
} elseif (preg_match('/^ANSWER:\s?([A-Z])\s?/', $info, $matches)) {
//the correct answers
$correct_answer_index = array_search($matches[1], $answers_array);
$exercise_info['question'][$question_index]['correct_answers'][] = $correct_answer_index + 1;
$correctAnswerIndex = array_search($matches[1], $answersArray);
$exerciseInfo['question'][$questionIndex]['correct_answers'][] = $correctAnswerIndex + 1;
//weight for correct answer
$exercise_info['question'][$question_index]['weighting'][$correct_answer_index] = 1;
$exerciseInfo['question'][$questionIndex]['weighting'][$correctAnswerIndex] = 1;
$next = $line + 1;
if (false !== strpos($data[$next], 'ANSWER_EXPLANATION:')) {
if (isset($data[$next]) && false !== strpos($data[$next], 'ANSWER_EXPLANATION:')) {
continue;
}
if (false !== strpos($data[$next], 'DESCRIPTION:')) {
if (isset($data[$next]) && false !== strpos($data[$next], 'DESCRIPTION:')) {
continue;
}
// Check if next has score, otherwise loop too next question.
if (false === strpos($data[$next], 'SCORE:')) {
$answers_array = [];
$question_index++;
if (isset($data[$next]) && false === strpos($data[$next], 'SCORE:')) {
$answersArray = [];
$questionIndex++;
continue;
}
} elseif (preg_match('/^SCORE:\s?(.*)/', $info, $matches)) {
$exercise_info['question'][$question_index]['score'] = (float) $matches[1];
$answers_array = [];
$question_index++;
$exerciseInfo['question'][$questionIndex]['score'] = (float) $matches[1];
$answersArray = [];
$questionIndex++;
continue;
} elseif (preg_match('/^DESCRIPTION:\s?(.*)/', $info, $matches)) {
$exercise_info['question'][$question_index]['description'] = $matches[1];
$exerciseInfo['question'][$questionIndex]['description'] = $matches[1];
$next = $line + 1;
if (false !== strpos($data[$next], 'ANSWER_EXPLANATION:')) {
if (isset($data[$next]) && false !== strpos($data[$next], 'ANSWER_EXPLANATION:')) {
continue;
}
// Check if next has score, otherwise loop too next question.
if (false === strpos($data[$next], 'SCORE:')) {
$answers_array = [];
$question_index++;
if (isset($data[$next]) && false === strpos($data[$next], 'SCORE:')) {
$answersArray = [];
$questionIndex++;
continue;
}
} elseif (preg_match('/^ANSWER_EXPLANATION:\s?(.*)/', $info, $matches)) {
// Comment of correct answer
$correct_answer_index = array_search($matches[1], $answers_array);
$exercise_info['question'][$question_index]['feedback'] = $matches[1];
$correctAnswerIndex = array_search($matches[1], $answersArray);
$exerciseInfo['question'][$questionIndex]['feedback'] = $matches[1];
$next = $line + 1;
// Check if next has score, otherwise loop too next question.
if (false === strpos($data[$next], 'SCORE:')) {
$answers_array = [];
$question_index++;
if (isset($data[$next]) && false === strpos($data[$next], 'SCORE:')) {
$answersArray = [];
$questionIndex++;
continue;
}
} elseif (preg_match('/^TEXTO_CORRECTA:\s?(.*)/', $info, $matches)) {
//Comment of correct answer (Spanish e-ducativa format)
$correct_answer_index = array_search($matches[1], $answers_array);
$exercise_info['question'][$question_index]['feedback'] = $matches[1];
$correctAnswerIndex = array_search($matches[1], $answersArray);
$exerciseInfo['question'][$questionIndex]['feedback'] = $matches[1];
} elseif (preg_match('/^T:\s?(.*)/', $info, $matches)) {
//Question Title
$correct_answer_index = array_search($matches[1], $answers_array);
$exercise_info['question'][$question_index]['title'] = $matches[1];
$correctAnswerIndex = array_search($matches[1], $answersArray);
$exerciseInfo['question'][$questionIndex]['title'] = $matches[1];
} elseif (preg_match('/^TAGS:\s?([A-Z])\s?/', $info, $matches)) {
//TAGS for chamilo >= 1.10
$exercise_info['question'][$question_index]['answer_tags'] = explode(',', $matches[1]);
$exerciseInfo['question'][$questionIndex]['answer_tags'] = explode(',', $matches[1]);
} elseif (preg_match('/^ETIQUETAS:\s?([A-Z])\s?/', $info, $matches)) {
//TAGS for chamilo >= 1.10 (Spanish e-ducativa format)
$exercise_info['question'][$question_index]['answer_tags'] = explode(',', $matches[1]);
} elseif (empty($info)) {
/*if (empty($exercise_info['question'][$question_index]['title'])) {
$exercise_info['question'][$question_index]['title'] = $info;
}
//moving to next question (tolerate \r\n or just \n)
if (empty($exercise_info['question'][$question_index]['correct_answers'])) {
error_log('Aiken: Error in question index '.$question_index.': no correct answer defined');
return 'ExerciseAikenErrorNoCorrectAnswerDefined';
}
if (empty($exercise_info['question'][$question_index]['answer'])) {
error_log('Aiken: Error in question index '.$question_index.': no answer option given');
return 'ExerciseAikenErrorNoAnswerOptionGiven';
}
$question_index++;
//emptying answers array when moving to next question
$answers_array = [];
//$new_question = true;*/
$exerciseInfo['question'][$questionIndex]['answer_tags'] = explode(',', $matches[1]);
} else {
if (empty($exercise_info['question'][$question_index]['title'])) {
$exercise_info['question'][$question_index]['title'] = $info;
if (empty($exerciseInfo['question'][$questionIndex]['title'])) {
$exerciseInfo['question'][$questionIndex]['title'] = $info;
}
/*$question_index++;
//emptying answers array when moving to next question
$answers_array = [];
$new_question = true;*/
}
}
$total_questions = count($exercise_info['question']);
$total_weight = !empty($_POST['total_weight']) ? (int) ($_POST['total_weight']) : 20;
foreach ($exercise_info['question'] as $key => $question) {
if (!isset($exercise_info['question'][$key]['weighting'])) {
$totalQuestions = count($exerciseInfo['question']);
$totalWeight = !empty($_POST['total_weight']) ? (int) ($_POST['total_weight']) : 20;
foreach ($exerciseInfo['question'] as $key => $question) {
if (!isset($exerciseInfo['question'][$key]['weighting'])) {
continue;
}
$exercise_info['question'][$key]['weighting'][current(array_keys($exercise_info['question'][$key]['weighting']))] = $total_weight / $total_questions;
$exerciseInfo['question'][$key]['weighting'][current(array_keys($exerciseInfo['question'][$key]['weighting']))] = $totalWeight / $totalQuestions;
}
}
/**
* Parses an Aiken file and builds an array of exercise + questions to be
* imported by the import_exercise() function.
*
* @param array The reference to the array in which to store the questions
* @param string Path to the directory with the file to be parsed (without final /)
* @param string Name of the last directory part for the file (without /)
* @param string Name of the file to be parsed (including extension)
* @param string $exercisePath
* @param string $file
* @param string $questionFile
*
* @return string|bool True on success, error message on error
* @assert ('','','') === false
*/
function aiken_parse_file(&$exercise_info, $exercisePath, $file, $questionFile)
{
$questionTempDir = $exercisePath.'/'.$file.'/';
$questionFilePath = $questionTempDir.$questionFile;
if (!is_file($questionFilePath)) {
return 'FileNotFound';
}
$text = file_get_contents($questionFilePath);
setExerciseInfoFromAikenText($text, $exercise_info);
//exit;
return true;
}
@ -451,7 +548,7 @@ function aiken_import_file($array_file)
}
if ($process && $unzip == 1) {
$imported = aiken_import_exercise($array_file['name']);
$imported = aikenImportExercise($array_file['name']);
if (is_numeric($imported) && !empty($imported)) {
Display::addFlash(Display::return_message(get_lang('Uploaded')));

@ -283,6 +283,7 @@ class AppPlugin
'whispeakauth',
'zoom',
'xapi',
'ai_helper',
];
return $officialPlugins;

@ -8969,4 +8969,13 @@ $RepeatXDays = "Every x days";
$NumberOfDays = "Number of days";
$WriteAComment = "Leave a comment";
$ProvideACommentFirst = "Please leave a comment first";
$QuizFinalizationDate = "Last quiz finalization date";
$LpFinalizationDate = "Last lp's finalization date";
$OnlyThoseThatCorrespondToAllTheSelectedCategories = "Must be in ALL the selected categories";
$reportByAttempts = "Report by attempts";
$QuestionsTopic = "Questions topic";
$QuestionsTopicHelp = "The questions topic will be used as the name of the test and will be sent to the AI generator to generate questions in the language configured for this course, asking for them to be generated in the Aiken format so they can easily be imported into this course.";
$AIQuestionsGenerator = "AI Questions Generator";
$AIQuestionsGeneratorNumberHelper = "Most AI generators are limited in the number of characters they can return, and often your organization will be charged based on the number of characters returned, so please use with moderation, asking for smaller numbers first, then extending as you gain confidence.
A good number for a first test is 3 questions.";
?>

@ -8904,4 +8904,12 @@ $RepeatXDays = "Tous les x jours";
$NumberOfDays = "Nombre de jours";
$WriteAComment = "Faire un commentaire";
$ProvideACommentFirst = "Veuillez d'abord écrire un commentaire";
$QuizFinalizationDate = "Dernière date de fin d'un exercice";
$LpFinalizationDate = "Dernière date de fin d'un parcours";
$OnlyThoseThatCorrespondToAllTheSelectedCategories = "Doit être dans TOUTES les catégories sélectionnées";
$reportByAttempts = "Rapport par tentative";
$QuestionsTopic = "Sujet des questions";
$QuestionsTopicHelp = "Le sujet des questions sera utilisé à la fois comme titre de l'exercice créé et comme requête envoyée au générateur de questions par Intelligence Artificielle (IA) pour qu'il génère des questions sur ce sujet dans la langue configurée de ce cours. Vous pourrez réviser ces propositions de questions ci-dessous avant de les importer.";
$AIQuestionsGenerator = "Générateur de questions par IA";
$AIQuestionsGeneratorNumberHelper = "La plupart des générateurs basés sur l'IA limitent le nombre de caractères qu'ils peuvent renvoyer, et votre organisation sera souvent facturée sur base du nombre de caractères renvoyés. Utilisez donc cette fonctionnalité avec modération, en demandant d'abord de petits nombres de questions, puis en augmentant quand vous commencer à prendre confiance. Un bon nombre de départ est de 3 questions.";
?>

@ -8995,4 +8995,12 @@ $RepeatXDays = "Cada x días";
$NumberOfDays = "Número de días";
$WriteAComment = "Dejar un comentario";
$ProvideACommentFirst = "Por favor deje un comentario primero";
$QuizFinalizationDate = "Última fecha de finalización de un ejercicio";
$LpFinalizationDate = "Última fecha de finalización de una lección";
$OnlyThoseThatCorrespondToAllTheSelectedCategories = "Tiene que estar en TODAS las categorias seleccionadas";
$reportByAttempts = "Reporte por intento";
$QuestionsTopic = "Tema de preguntas";
$QuestionsTopicHelp = "El tema de las preguntas será usado de un lado como título del ejercicio creado, y de otro lado para enviar al generador de inteligencia artificial (IA) para que genere preguntas en el idioma de este curso, pidiendo también que estén generadas en el formato Aiken para poder importarlas en el curso. Podrá validar/modificar las propuestas de preguntas a bajo antes de finalizar la importación.";
$AIQuestionsGenerator = "Generador de preguntas por IA";
$AIQuestionsGeneratorNumberHelper = "La mayoría de los generadores de IA son limitados en número de caracteres que pueden devolver, y vuestra organización muchas veces será facturada en base a la cantidad de caracteres devueltos. Por lo tanto, recomendamos proceder con moderación, solicitando bajas cantidades de preguntas en un primer momento, y extendiendo mientra va tomando confianza. Un buen número inicial es de 3 preguntas.";
?>

@ -0,0 +1,83 @@
<?php
/* For license terms, see /license.txt */
/**
* Description of AiHelperPlugin.
*
* @author Christian Beeznest <christian.fasanando@beeznest.com>
*/
class AiHelperPlugin extends Plugin
{
public const OPENAI_API = 'openai';
protected function __construct()
{
$version = '1.0';
$author = 'Christian Beeznest';
$message = 'Description';
$settings = [
$message => 'html',
'tool_enable' => 'boolean',
'api_name' => [
'type' => 'select',
'options' => $this->getApiList(),
],
'api_key' => 'text',
'organization_id' => 'text',
];
parent::__construct($version, $author, $settings);
}
/**
* Get the list of apis availables.
*
* @return array
*/
public function getApiList()
{
$list = [
self::OPENAI_API => 'OpenAI',
];
return $list;
}
/**
* Get the plugin directory name.
*/
public function get_name(): string
{
return 'ai_helper';
}
/**
* Get the class instance.
*
* @staticvar AiHelperPlugin $result
*/
public static function create(): AiHelperPlugin
{
static $result = null;
return $result ?: $result = new self();
}
/**
* Install the plugin. Set the database up.
*/
public function install()
{
}
/**
* Unistall plugin. Clear the database.
*/
public function uninstall()
{
}
}

@ -0,0 +1,13 @@
AI Helper plugin
======
Version 1.0
> This plugin is meant to be later integrated into Chamilo (in a major version
release).
The AI helper plugin integrates into parts of the platform that seem the most useful to teachers/trainers or learners.
Because available Artificial Intelligence (to use the broad term) now allows us to ask for meaningful texts to be generated, we can use those systems to pre-generate content, then let the teacher/trainer review the content before publication.
Currently, this plugin is only integrated into:
- exercises: in the Aiken import form, scrolling down

@ -0,0 +1,16 @@
<?php
/* For license terms, see /license.txt */
/**
* Install the Ai Helper Plugin.
*
* @package chamilo.plugin.ai_helper
*/
require_once __DIR__.'/../../main/inc/global.inc.php';
require_once __DIR__.'/AiHelperPlugin.php';
if (!api_is_platform_admin()) {
exit('You must have admin permissions to install plugins');
}
AiHelperPlugin::create()->install();

@ -0,0 +1,13 @@
<?php
/* For license terms, see /license.txt */
$strings['plugin_title'] = 'AI Helper plugin';
$strings['plugin_comment'] = 'Integrates into parts of the platform that seem the most useful to teachers/trainers or learners';
$strings['Description'] = 'Available Artificial Intelligence services (to use the broad term) now allows you to ask for meaningful texts to be generated, we can use those systems to pre-generate content, then let the teacher/trainer review the content before publication.';
$strings['tool_enable'] = 'Enable plugin';
$strings['api_name'] = 'AI API to use';
$strings['api_key'] = 'Api key';
$strings['api_key_help'] = 'Secret key generated for your Ai api';
$strings['organization_id'] = 'Organization ID';
$strings['organization_id_help'] = 'In case your api account is from an organization.';
$strings['OpenAI'] = 'OpenAI';

@ -0,0 +1,13 @@
<?php
/* For license terms, see /license.txt */
$strings['plugin_title'] = 'AI Helper plugin.';
$strings['plugin_comment'] = 'S\'intègre dans les parties de la plateforme qui semblent les plus utiles aux enseignants.';
$strings['Description'] = 'L\'intelligence artificielle disponible (pour utiliser le terme large) nous permet désormais de demander la génération de textes significatifs, nous pouvons utiliser ces systèmes pour pré-générer du contenu, et laisser l\'enseignant/formateur perfectionner le contenu avant publication.';
$strings['tool_enable'] = 'Activer le plug-in';
$strings['api_name'] = 'Ai Api pour se connecter';
$strings['api_key'] = 'Api key';
$strings['api_key_help'] = 'Clé secrète générée pour votre API Ai';
$strings['organization_id'] = 'Organization ID';
$strings['organization_id_help'] = 'Si votre compte api provient d\'une organisation.';
$strings['OpenAI'] = 'OpenAI';

@ -0,0 +1,13 @@
<?php
/* For license terms, see /license.txt */
$strings['plugin_title'] = 'AI Helper plugin.';
$strings['plugin_comment'] = 'Se integra en partes de la plataforma que parecen más útiles para profesores/formadores o alumnos.';
$strings['Description'] = 'Debido a que la Inteligencia Artificial disponible (para usar el término amplio) ahora nos permite solicitar que se generen textos significativos, podemos usar esos sistemas para generar contenido previamente y luego dejar que el maestro/entrenador revise el contenido antes de publicarlo..';
$strings['tool_enable'] = 'Habilitar complemento';
$strings['api_name'] = 'Ai API para conectar';
$strings['api_key'] = 'Api key';
$strings['api_key_help'] = 'Clave secreta generada para su Ai api';
$strings['organization_id'] = 'Identificación de la organización';
$strings['organization_id_help'] = 'En caso de que su cuenta api sea de una organización.';
$strings['OpenAI'] = 'OpenAI';

@ -0,0 +1,6 @@
<?php
/* For license terms, see /license.txt */
require_once __DIR__.'/AiHelperPlugin.php';
$plugin_info = AiHelperPlugin::create()->get_info();

@ -0,0 +1,332 @@
<?php
/* For licensing terms, see /license.txt */
require_once 'Url.php';
class OpenAi
{
private $model = "text-davinci-003";
private $headers;
private $contentTypes;
private $timeout = 0;
private $streamMethod;
public function __construct(
string $apiKey,
string $organizationId = ''
) {
$this->contentTypes = [
"application/json" => "Content-Type: application/json",
"multipart/form-data" => "Content-Type: multipart/form-data",
];
$this->headers = [
$this->contentTypes["application/json"],
"Authorization: Bearer $apiKey",
];
if (!empty($organizationId)) {
$this->headers[] = "OpenAI-Organization: $organizationId";
}
}
/**
* @return bool|string
*/
public function listModels()
{
$url = Url::fineTuneModel();
return $this->sendRequest($url, 'GET');
}
/**
* @param $model
*
* @return bool|string
*/
public function retrieveModel($model)
{
$model = "/$model";
$url = Url::fineTuneModel().$model;
return $this->sendRequest($url, 'GET');
}
/**
* @param $opts
* @param null $stream
*
* @return bool|string
*/
public function completion($opts, $stream = null)
{
if ($stream != null && array_key_exists('stream', $opts)) {
if (!$opts['stream']) {
throw new Exception('Please provide a stream function.');
}
$this->streamMethod = $stream;
}
$opts['model'] = $opts['model'] ?? $this->model;
$url = Url::completionsURL();
return $this->sendRequest($url, 'POST', $opts);
}
/**
* @param $opts
*
* @return bool|string
*/
public function createEdit($opts)
{
$url = Url::editsUrl();
return $this->sendRequest($url, 'POST', $opts);
}
/**
* @param $opts
*
* @return bool|string
*/
public function image($opts)
{
$url = Url::imageUrl()."/generations";
return $this->sendRequest($url, 'POST', $opts);
}
/**
* @param $opts
*
* @return bool|string
*/
public function imageEdit($opts)
{
$url = Url::imageUrl()."/edits";
return $this->sendRequest($url, 'POST', $opts);
}
/**
* @param $opts
*
* @return bool|string
*/
public function createImageVariation($opts)
{
$url = Url::imageUrl()."/variations";
return $this->sendRequest($url, 'POST', $opts);
}
/**
* @param $opts
*
* @return bool|string
*/
public function moderation($opts)
{
$url = Url::moderationUrl();
return $this->sendRequest($url, 'POST', $opts);
}
/**
* @param $opts
*
* @return bool|string
*/
public function uploadFile($opts)
{
$url = Url::filesUrl();
return $this->sendRequest($url, 'POST', $opts);
}
/**
* @return bool|string
*/
public function listFiles()
{
$url = Url::filesUrl();
return $this->sendRequest($url, 'GET');
}
/**
* @param $fileId
*
* @return bool|string
*/
public function retrieveFile($fileId)
{
$fileId = "/$fileId";
$url = Url::filesUrl().$fileId;
return $this->sendRequest($url, 'GET');
}
/**
* @param $fileId
*
* @return bool|string
*/
public function retrieveFileContent($fileId)
{
$fileId = "/$fileId/content";
$url = Url::filesUrl().$fileId;
return $this->sendRequest($url, 'GET');
}
/**
* @param $fileId
*
* @return bool|string
*/
public function deleteFile($fileId)
{
$fileId = "/$fileId";
$url = Url::filesUrl().$fileId;
return $this->sendRequest($url, 'DELETE');
}
/**
* @param $opts
*
* @return bool|string
*/
public function createFineTune($opts)
{
$url = Url::fineTuneUrl();
return $this->sendRequest($url, 'POST', $opts);
}
/**
* @return bool|string
*/
public function listFineTunes()
{
$url = Url::fineTuneUrl();
return $this->sendRequest($url, 'GET');
}
/**
* @param $fineTuneId
*
* @return bool|string
*/
public function retrieveFineTune($fineTuneId)
{
$fineTuneId = "/$fineTuneId";
$url = Url::fineTuneUrl().$fineTuneId;
return $this->sendRequest($url, 'GET');
}
/**
* @param $fineTuneId
*
* @return bool|string
*/
public function cancelFineTune($fineTuneId)
{
$fineTuneId = "/$fineTuneId/cancel";
$url = Url::fineTuneUrl().$fineTuneId;
return $this->sendRequest($url, 'POST');
}
/**
* @param $fineTuneId
*
* @return bool|string
*/
public function listFineTuneEvents($fineTuneId)
{
$fineTuneId = "/$fineTuneId/events";
$url = Url::fineTuneUrl().$fineTuneId;
return $this->sendRequest($url, 'GET');
}
/**
* @param $fineTuneId
*
* @return bool|string
*/
public function deleteFineTune($fineTuneId)
{
$fineTuneId = "/$fineTuneId";
$url = Url::fineTuneModel().$fineTuneId;
return $this->sendRequest($url, 'DELETE');
}
/**
* @param $opts
*
* @return bool|string
*/
public function embeddings($opts)
{
$url = Url::embeddings();
return $this->sendRequest($url, 'POST', $opts);
}
public function setTimeout(int $timeout)
{
$this->timeout = $timeout;
}
private function sendRequest(
string $url,
string $method,
array $opts = []
) {
$post_fields = json_encode($opts);
if (array_key_exists('file', $opts) || array_key_exists('image', $opts)) {
$this->headers[0] = $this->contentTypes["multipart/form-data"];
$post_fields = $opts;
} else {
$this->headers[0] = $this->contentTypes["application/json"];
}
$curl_info = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $post_fields,
CURLOPT_HTTPHEADER => $this->headers,
];
if ($opts == []) {
unset($curl_info[CURLOPT_POSTFIELDS]);
}
if (array_key_exists('stream', $opts) && $opts['stream']) {
$curl_info[CURLOPT_WRITEFUNCTION] = $this->streamMethod;
}
$curl = curl_init();
curl_setopt_array($curl, $curl_info);
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
}

@ -0,0 +1,74 @@
<?php
/* For licensing terms, see /license.txt */
class Url
{
public const ORIGIN = 'https://api.openai.com';
public const API_VERSION = 'v1';
public const OPEN_AI_URL = self::ORIGIN."/".self::API_VERSION;
public static function completionsURL(): string
{
return self::OPEN_AI_URL."/completions";
}
public static function editsUrl(): string
{
return self::OPEN_AI_URL."/edits";
}
public static function searchURL(string $engine): string
{
return self::OPEN_AI_URL."/engines/$engine/search";
}
public static function enginesUrl(): string
{
return self::OPEN_AI_URL."/engines";
}
public static function engineUrl(string $engine): string
{
return self::OPEN_AI_URL."/engines/$engine";
}
public static function classificationsUrl(): string
{
return self::OPEN_AI_URL."/classifications";
}
public static function moderationUrl(): string
{
return self::OPEN_AI_URL."/moderations";
}
public static function filesUrl(): string
{
return self::OPEN_AI_URL."/files";
}
public static function fineTuneUrl(): string
{
return self::OPEN_AI_URL."/fine-tunes";
}
public static function fineTuneModel(): string
{
return self::OPEN_AI_URL."/models";
}
public static function answersUrl(): string
{
return self::OPEN_AI_URL."/answers";
}
public static function imageUrl(): string
{
return self::OPEN_AI_URL."/images";
}
public static function embeddings(): string
{
return self::OPEN_AI_URL."/embeddings";
}
}

@ -0,0 +1,68 @@
<?php
/* For license terms, see /license.txt */
/**
Answer questions based on existing knowledge.
*/
require_once __DIR__.'/../../../main/inc/global.inc.php';
require_once __DIR__.'/../AiHelperPlugin.php';
require_once __DIR__.'/../src/openai/OpenAi.php';
$plugin = AiHelperPlugin::create();
$apiList = $plugin->getApiList();
$apiName = $plugin->get('api_name');
if (!in_array($apiName, array_keys($apiList))) {
throw new Exception("Ai API is not available for this request.");
}
switch ($apiName) {
case AiHelperPlugin::OPENAI_API:
$questionTypes = [
'multiple_choice' => 'multiple choice',
'unique_answer' => 'unique answer',
];
$nQ = (int) $_REQUEST['nro_questions'];
$lang = (string) $_REQUEST['language'];
$topic = (string) $_REQUEST['quiz_name'];
$questionType = $questionTypes[$_REQUEST['question_type']] ?? $questionTypes['multiple_choice'];
$prompt = 'Generate %d "%s" questions in Aiken format in the %s language about "%s", making sure there is a \'ANSWER\' line for each question. \'ANSWER\' lines must only mention the letter of the correct answer, not the full answer text and not a parenthesis. The response line must not be separated from the last answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text. Include an \'ANSWER_EXPLANATION\' line after the \'ANSWER\' line for each question. The terms between single quotes above must not be translated. There must be a blank line between each question. Show the question directly without any prefix. Each answer must not be quoted.';
$apiKey = $plugin->get('api_key');
$organizationId = $plugin->get('organization_id');
$ai = new OpenAi($apiKey, $organizationId);
$temperature = 0.2;
$model = 'text-davinci-003';
$maxTokens = 2000;
$frequencyPenalty = 0;
$presencePenalty = 0.6;
$prompt = sprintf($prompt, $nQ, $questionType, $lang, $topic);
$complete = $ai->completion([
'model' => $model,
'prompt' => $prompt,
'temperature' => $temperature,
'max_tokens' => $maxTokens,
'frequency_penalty' => $frequencyPenalty,
'presence_penalty' => $presencePenalty,
]);
$result = json_decode($complete, true);
// Returns the text answers generated.
$return = ['success' => false, 'text' => ''];
if (!empty($result['choices'])) {
$return = [
'success' => true,
'text' => trim($result['choices'][0]['text']),
];
}
echo json_encode($return);
break;
}

@ -0,0 +1,16 @@
<?php
/* For license terms, see /license.txt */
/**
* Uninstall the Ai Helper Plugin.
*
* @package chamilo.plugin.ai_helper
*/
require_once __DIR__.'/../../main/inc/global.inc.php';
require_once __DIR__.'/AiHelperPlugin.php';
if (!api_is_platform_admin()) {
exit('You must have admin permissions to install plugins');
}
AiHelperPlugin::create()->uninstall();
Loading…
Cancel
Save