Merge branch 'CT-4534-openai' of https://github.com/christianbeeznest/chamilo-lms into 4539

pull/4550/head
Yannick Warnier 3 years ago
commit 1ce21ae91d
  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. 83
      plugin/ai_helper/AiHelperPlugin.php
  5. 10
      plugin/ai_helper/README.md
  6. 16
      plugin/ai_helper/install.php
  7. 13
      plugin/ai_helper/lang/english.php
  8. 13
      plugin/ai_helper/lang/french.php
  9. 13
      plugin/ai_helper/lang/spanish.php
  10. 6
      plugin/ai_helper/plugin.php
  11. 332
      plugin/ai_helper/src/openai/OpenAi.php
  12. 74
      plugin/ai_helper/src/openai/Url.php
  13. 68
      plugin/ai_helper/tool/answers.php
  14. 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('Topic'));
$form->addRule('quiz_name', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('number', 'nro_questions', get_lang('NumberOfQuestions'));
$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;

@ -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 = Display::return_message($this->get_lang('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 => $this->get_lang('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,10 @@
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.

@ -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'] = 'Due 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.';
$strings['tool_enable'] = 'Enable plugin';
$strings['api_name'] = 'Ai Api to connect';
$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, puis laisser l\'enseignant/formateur réviser 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.';
$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