Merge branch 'deepseek' into 1.11.x

pull/6052/head
Yannick Warnier 8 months ago
commit 1b935a872e
  1. 47
      main/exercise/export/aiken/aiken_import.inc.php
  2. 1
      main/inc/lib/display.lib.php
  3. 4
      main/lang/dutch/trad4all.inc.php
  4. 2
      main/lang/english/trad4all.inc.php
  5. 2
      main/lang/french/trad4all.inc.php
  6. 2
      main/lang/spanish/trad4all.inc.php
  7. 118
      main/lp/LpAiHelper.php
  8. 256
      plugin/ai_helper/AiHelperPlugin.php
  9. 80
      plugin/ai_helper/README.md
  10. 4
      plugin/ai_helper/lang/english.php
  11. 3
      plugin/ai_helper/lang/spanish.php
  12. 87
      plugin/ai_helper/src/deepseek/DeepSeek.php
  13. 17
      plugin/ai_helper/src/deepseek/Url.php
  14. 37
      plugin/ai_helper/src/openai/OpenAi.php
  15. 64
      plugin/ai_helper/tool/answers.php
  16. 357
      plugin/ai_helper/tool/learnpath.php

@ -44,8 +44,8 @@ function aiken_display_form()
}
/**
* Generates aiken format using AI api.
* Requires plugin ai_helper to connect to the api.
* Generates Aiken format using AI APIs (supports multiple providers).
* Requires plugin ai_helper to connect to the API.
*/
function generateAikenForm()
{
@ -53,6 +53,12 @@ function generateAikenForm()
return false;
}
$plugin = AiHelperPlugin::create();
$availableApis = $plugin->getApiList();
$configuredApi = $plugin->get('api_name');
$hasSingleApi = count($availableApis) === 1 || isset($availableApis[$configuredApi]);
$form = new FormValidator(
'aiken_generate',
'post',
@ -60,20 +66,31 @@ function generateAikenForm()
null
);
$form->addElement('header', get_lang('AIQuestionsGenerator'));
$form->addElement('text', 'quiz_name', [get_lang('QuestionsTopic'), get_lang('QuestionsTopicHelp')]);
if ($hasSingleApi) {
$apiName = $availableApis[$configuredApi] ?? $configuredApi;
$form->addHtml('<div style="margin-bottom: 10px; font-size: 14px; color: #555;">'
. sprintf(get_lang('UsingAIProviderX'), '<strong>'.htmlspecialchars($apiName).'</strong>').'</div>');
}
$form->addElement('text', 'quiz_name', get_lang('QuestionsTopic'));
$form->addRule('quiz_name', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('number', 'nro_questions', [get_lang('NumberOfQuestions'), get_lang('AIQuestionsGeneratorNumberHelper')]);
$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
);
$form->addElement('select', 'question_type', get_lang('QuestionType'), $options);
if (!$hasSingleApi) {
$form->addElement(
'select',
'ai_provider',
get_lang('AIProvider'),
array_combine(array_keys($availableApis), array_keys($availableApis))
);
}
$generateUrl = api_get_path(WEB_PLUGIN_PATH).'ai_helper/tool/answers.php';
$language = api_get_interface_language();
@ -87,10 +104,9 @@ function generateAikenForm()
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);
var qWeight = 1;
var qType = $("[name=\'question_type\']").val();'
. (!$hasSingleApi ? 'var provider = $("[name=\'ai_provider\']").val();' : 'var provider = "' . $configuredApi . '";') .
'var valid = (quizName != \'\' && nroQ > 0);
if (valid) {
btnGenerate.attr("disabled", true);
btnGenerate.text("'.get_lang('PleaseWaitThisCouldTakeAWhile').'");
@ -100,7 +116,8 @@ function generateAikenForm()
"quiz_name": quizName,
"nro_questions": nroQ,
"question_type": qType,
"language": "'.$language.'"
"language": "'.$language.'",
"ai_provider": provider
}).done(function (data) {
btnGenerate.attr("disabled", false);
btnGenerate.text("'.get_lang('Generate').'");

@ -946,7 +946,6 @@ class Display
{
if (!empty($url)) {
$url = preg_replace('#&amp;#', '&', $url);
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$attributes['href'] = $url;
}

@ -5145,7 +5145,7 @@ $DescriptionContent = "<p>Deze module zal u helpen uw cursus op een gemakkelijke
$LinksContent = "<p>De linkmodule laat u toe een verzameling cursusmateriaal aan te leggen voor de studenten, vooral materiaal dat u niet zelf hebt aangemaakt.</p><p>Wanneer de lijst aangroeit, kan het relevant zijn om ze te organiseren in categorieën. U kan iedere link wijzigen om hem toe te kennen aan een categorie (u moet wel eerst een categorie aanmaken).</p><p>Het tekstvak 'Omschrijving' kan gebruikt worden om informatie te geven over het doel van websites maar ook om mee te geven wat de student met de link moet doen. Als, bijvoorbeeld, u verwijst naar een website op Aristoteles, kan u de student vragen om het verschil tussen synthese en analyse te bestuderen.</p>";
$MycoursesContent = "<p>Eenmaal ingelogd bent u op uw <em>persoonlijke startpagina</em>.</p><p>In het hoofdgedeelte (centrum) ziet u \"<b>Mijn cursussen</b>\", een lijst met al uw cursussen. Afhankelijk van uw gebruikersrechten kunt u ook de mogelijkheid hebben om nieuwe cursussen aan te maken (via het menu aan de rechterkant van de pagina).</p><p>Bovenaan ziet u<ul>\t<li>\"<b>Mijn profiel</b>\": hier kunt u verschillende zaken (naam, voornaam, e-mailadres…) wijzigen. U kunt ook de statistieken van uw platformgebruik opvragen.</li>\t<li>\"<b>Mijn agenda</b>\": dit bevat de agenda-items van al uw cursussen.</li></ul></p><p><b>Wijzig mijn cursuslijst</b>, in het menu aan de rechterkant, laat u toe in te schrijven voor nieuwe cursussen. Deze link laat u ook uitschrijven uit cursussen.</p><p>De links <b>Ondersteuningsforum</b> en <b>Documentatie</b> verwijzen naar de Chamilowebsite, waar u terecht kunt voor technische informatie of pedagogisch advies.</p><p>Om naar een cursus te gaan, klikt u op de naam. Uw status als cursusgebruiker kan variëren van de ene cursus tot de andere. Het is mogelijk dat u cursusbeheerder (lesgever) bent in een cursus, en student (leerling) in een andere.</p>";
$AgendaContent = "<p>De agenda verschijnt niet alleen in elke cursus zelf, maar ook als een overzichtsagenda voor de student ('Mijn agenda' in de menubalk bovenaan).</p><p>Binnen de cursus ziet de agenda eruit als een lijst met evenementen. U kan documenten of activiteiten met een agenda-item verbinden, zodat de agenda een chronologisch programma wordt voor uw leeractiviteiten.</p><p>Nieuwe agenda-items staan niet alleen in de agenda zelf, maar de student krijgt ook een aanduiding ervan wanneer hij/zij opnieuw inlogt. Het systeem geeft nieuwe items (sedert vorig bezoek) in de agenda (en de aankondigingen) aan: op de openingspagina ziet de student een icoontje naast alle cursussen waaraan een agenda-item of aankondiging is toegevoegd.</p><p>Wilt u werkelijk een chronologische structuur aanbrengen in de leeractiviteiten, dan kan u beter het leerpad gebruiken: dat gebruikt gelijkaardige principes, maar met meer geavanceerde mogelijkheden. Het leerpad kan beschouwd worden als een synthese van een inhoudstafel + een agenda + sequentie (opgelegde volgorde) + tracking.</p>";
$AnnouncementsContent = "<p>Met aankondigingen kan je een e-mailbericht sturen naar alle ingeschreven studenten, naar sommige ervan, of naar een of meer groepen. Zo kan je studenten die niet geregeld de cursuspagina's bezoeken daartoe aansporen.</p><p>Kies 'Aankondiging toevoegen' en selecteer groepen/gebruikers. Gebruik <span style=\"border: 1px solid; border-color: inherit;\"> Ctrl </span>+klik om er meerdere te kiezen, \">>\" om ze aan de bestemmelingen toe te voegen. Vink het hokje aan, vul onderwerp en bericht in en klik OK.</p>";
$AnnouncementsContent = "<p>Met aankondigingen kan je een e-mailbericht sturen naar alle ingeschreven studenten, naar sommige ervan, of naar een of meer groepen. Zo kan je studenten die niet geregeld de cursuspagina's bezoeken daartoe aansporen.</p><p>Kies 'Aankondiging toevoegen' en selecteer groepen/gebruikers. Gebruik <span style=\"border: 1px solid; border-color: inherit;\"> Ctrl </span>+klik om er meerdere te kiezen, \"&gt;&gt;\" om ze aan de bestemmelingen toe te voegen. Vink het hokje aan, vul onderwerp en bericht in en klik OK.</p>";
$ChatContent = "<p>In deze chatbox kunt u live met uw studenten discussiëren.</p><p>Deze chatfunctie werkt anders dan <acronym title=\"instant messaging\" xml:lang=\"en\" lang=\"en\">IM</acronym>-programma's (zoals MSN® of Yahoo Messenger®) omdat deze chat webgebaseerd is. Het nadeel daarvan is dat het bijwerken (refresh) slechts om de 10 seconden gebeurt, en niet onmiddellijk. Maar de voordelen zijn dat de chat geïntegreerd is in uw cursus, dat uw discussies worden opgeslagen in een map in de documentenmodule, en dat uw studenten geen extra plug-in hoeven te downloaden en te installeren.</p><p>Indien een gebruiker zijn/haar fotootje heeft toegevoegd bij 'Mijn profiel' (menubalk bovenaan), dan zal deze foto in de chatconversatie verschijnen, zodat hij/zij herkenbaar wordt.</p><p>Alleen de cursusbeheerder kan de discussie verwijderen wanneer hij/zij dit verkiest.</p><b>Pedagogische relevantie</b><p>Een chatfunctie in uw cursus heeft niet altijd een toegevoegde waarde, maar indien de chat in uw scenario past, kan de functie nuttig zijn. De docent kan de chatfunctie bijvoorbeeld verbergen, en alleen zichtbaar maken op bepaalde afgesproken tijdstippen, wanneer hij/zij live op vragen en opmerkingen van studenten wenst in te gaan. In een dergelijk scenario is de chat geen 'vrij discussiepaneel', terwijl studenten toch hun voordeel kunnen doen bij een liveontmoeting met de docent.</p>";
$WorkContent = "<p>Hier kunnen studenten documenten plaatsen. Via deze pagina is het mogelijk documenten uit te wisselen tussen studenten, studentengroepen en lesgevers. Als u via de groepsruimte een document publiceert (optie = publiceren), zal een verwijzing aangemaakt worden naar uw document, zonder dit te verplaatsen.</p><p>U kan kiezen of door de studenten geplaatste documenten standaard zichtbaar of onzichtbaar zijn.</p><p>Deze module heeft een <em>Introductietekst</em>gebied waarin u een vraag, richtlijnen, deadlines of andere zaken kan noteren.</p>";
$TrackingContent = "Statistieken helpen u om uw studenten op te volgen: hebben ze ingelogd op het systeem, wanneer en hoe vaak deden ze dat? Hebben ze hun scriptie al geüpload? Wanneer? Als u <acronym title=\"Sharable Content Object Reference Model\" xml:lang=\"en\" lang=\"en\">SCORM</acronym>-cursussen gebruikt, kan u zelfs nagaan hoeveel tijd een student gespendeerd heeft aan een module of hoofdstuk. De statistieken leveren informatie op twee niveaus:<ul>\t<li><b>Globaal</b>: Hoeveel studenten bezochten de cursus? Wat zijn de meest bezochte pagina's en links?</li>\t<li><b>Nominatief</b>: Welke pagina heeft Jan Janssens bezocht? Welke score behaalde hij op zijn tests? Wanneer heeft hij laatst ingelogd op het systeem?</li></ul>";
@ -6910,4 +6910,4 @@ $AddEmailPicture = "Voeg site & e-mail banner toe";
$AddEmailPictureComment = "De foto moet een minimale verhouding hebben van 25/7, u kunt de foto die u uploadt bijsnijden.";
$DeleteEmailPicture = "Verwijder site & e-mail banner";
$AddPictureComment = "De afbeelding moet een verhouding van 16/9 hebben, u kunt de afbeelding die u uploadt bijsnijden.";
?>
?>

@ -9078,4 +9078,6 @@ $DueToInactivityTheSessionIsGoingToClose = "Due to your inactivity, this session
$KeepGoing = "Keep going";
$SessionIsClosing = "Your session is closing";
$CannotChangeVisibilityOfBaseCourseResourceX = "The visibility of %s from the base course cannot be changed from the session.";
$UsingAIProviderX = "Using AI provider %s";
$AIProvider = "AI provider";
?>

@ -9013,4 +9013,6 @@ $DueToInactivityTheSessionIsGoingToClose = "Dû à votre inactivité, la session
$KeepGoing = "Rester connecté";
$SessionIsClosing = "Votre session est en cours de fermeture";
$CannotChangeVisibilityOfBaseCourseResourceX = "La visibilité de %s du cours de base ne peut être changée depuis une session.";
$UsingAIProviderX = "Utilisation du fournisseur d'IA %s";
$AIProvider = "Fournisseur d'IA";
?>

@ -9103,4 +9103,6 @@ $DueToInactivityTheSessionIsGoingToClose = "Debido a su inactividad, esta sesió
$KeepGoing = "Seguir conectado";
$SessionIsClosing = "Su sesión se está cerrando";
$CannotChangeVisibilityOfBaseCourseResourceX = "La visibilidad de %s del curso base no puede ser cambiada desde una sesión.";
$UsingAIProviderX = "Usando el proveedor de IA %s";
$AIProvider = "Proveedor de IA";
?>

@ -19,6 +19,11 @@ class LpAiHelper
*/
public function aiHelperForm()
{
$plugin = AiHelperPlugin::create();
$availableApis = $plugin->getApiList();
$configuredApi = $plugin->get('api_name');
$hasSingleApi = count($availableApis) === 1 || isset($availableApis[$configuredApi]);
$form = new FormValidator(
'lp_ai_generate',
'post',
@ -26,6 +31,13 @@ class LpAiHelper
null
);
$form->addElement('header', get_lang('LpAiGenerator'));
if ($hasSingleApi) {
$apiName = $availableApis[$configuredApi] ?? $configuredApi;
$form->addHtml('<div style="margin-bottom: 10px; font-size: 14px; color: #555;">'
.sprintf(get_lang('UsingAIProviderX'), '<strong>'.htmlspecialchars($apiName).'</strong>').'</div>');
}
$form->addElement('text', 'lp_name', [get_lang('LpAiTopic'), get_lang('LpAiTopicHelp')]);
$form->addRule('lp_name', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('number', 'nro_items', [get_lang('LpAiNumberOfItems'), get_lang('LpAiNumberOfItemsHelper')]);
@ -46,75 +58,55 @@ class LpAiHelper
$sessionId = api_get_session_id();
$redirectSuccess = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq().'&action=add_item&type=step&isStudentView=false&lp_id=';
$form->addHtml('<script>
$(function () {
$("#lp-quiz-area").hide();
$("#add-lp-quiz").change(function() {
if ($(this).is(":checked")) {
$("#lp-quiz-area").show();
} else {
$("#lp-quiz-area").hide();
}
});
$(function () {
$("#lp-quiz-area").hide();
$("#add-lp-quiz").change(function() {
$("#lp-quiz-area").toggle(this.checked);
});
$("#create-lp-ai").on("click", function (e) {
e.preventDefault();
e.stopPropagation();
$("#create-lp-ai").on("click", function (e) {
e.preventDefault();
e.stopPropagation();
var btnGenerate = $(this);
var lpName = $("[name=\'lp_name\']").val();
var nroItems = parseInt($("[name=\'nro_items\']").val());
var wordsCount = parseInt($("[name=\'words_count\']").val());
var valid = (lpName != \'\' && nroItems > 0 && wordsCount > 0);
var addTests = $("#add-lp-quiz").is(":checked");
var nroQuestions = parseInt($("[name=\'nro_questions\']").val());
var btnGenerate = $(this);
var lpName = $("[name=\'lp_name\']").val();
var nroItems = parseInt($("[name=\'nro_items\']").val());
var wordsCount = parseInt($("[name=\'words_count\']").val());
var addTests = $("#add-lp-quiz").is(":checked");
var nroQuestions = parseInt($("[name=\'nro_questions\']").val());
var provider = "' . $configuredApi . '";
if (valid) {
if (addTests) {
var quizValid = (nroQuestions > 0 && nroQuestions <= 5);
if (!quizValid) {
alert("'.sprintf(get_lang('NumberOfQuestionsLimitedFromXToY'), 1, 5).'");
return false;
}
}
btnGenerate.attr("disabled", true);
btnGenerate.text("'.get_lang('PleaseWaitThisCouldTakeAWhile').'");
$.getJSON("'.$generateUrl.'", {
"lp_name": lpName,
"nro_items": nroItems,
"words_count": wordsCount,
"language": "'.$language.'",
"course_code": "'.$courseCode.'",
"session_id": "'.$sessionId.'",
"add_tests": addTests,
"nro_questions": nroQuestions
}).done(function (data) {
btnGenerate.attr("disabled", false);
btnGenerate.text("'.get_lang('Generate').'");
if (data.success && data.success == true) {
if (lpName && nroItems > 0 && wordsCount > 0) {
if (addTests && (nroQuestions <= 0 || nroQuestions > 5)) {
alert("'.sprintf(get_lang('NumberOfQuestionsLimitedFromXToY'), 1, 5).'");
return false;
}
btnGenerate.attr("disabled", true).text("'.get_lang('PleaseWaitThisCouldTakeAWhile').'");
$.getJSON("'.$generateUrl.'", {
"lp_name": lpName,
"nro_items": nroItems,
"words_count": wordsCount,
"language": "'.$language.'",
"course_code": "'.$courseCode.'",
"session_id": "'.$sessionId.'",
"add_tests": addTests,
"nro_questions": nroQuestions,
"ai_provider": provider
}).done(function (data) {
btnGenerate.attr("disabled", false).text("'.get_lang('Generate').'");
if (data.success) {
location.href = "'.$redirectSuccess.'" + data.lp_id;
} else {
var errorMessage = "'.get_lang('NoSearchResults').'. '.get_lang('PleaseTryAgain').'";
if (data.text) {
errorMessage = data.text;
}
alert(errorMessage);
}
});
}
} else {
alert(data.text || "'.get_lang('NoSearchResults').'. '.get_lang('PleaseTryAgain').'");
}
});
});
</script>');
$form->addButton(
'create_lp_button',
get_lang('LearnpathAddLearnpath'),
'',
'default',
'default',
null,
['id' => 'create-lp-ai']
);
}
});
});
</script>');
$form->addButton('create_lp_button', get_lang('LearnpathAddLearnpath'), '', 'default', 'default', null, ['id' => 'create-lp-ai']);
echo $form->returnForm();
}
}

@ -3,20 +3,21 @@
use Chamilo\PluginBundle\Entity\AiHelper\Requests;
use Doctrine\ORM\Tools\SchemaTool;
require_once __DIR__ . '/src/deepseek/DeepSeek.php';
/**
* Description of AiHelperPlugin.
*
* @author Christian Beeznest <christian.fasanando@beeznest.com>
* @author Christian Beeznest
*/
class AiHelperPlugin extends Plugin
{
public const TABLE_REQUESTS = 'plugin_ai_helper_requests';
public const OPENAI_API = 'openai';
public const DEEPSEEK_API = 'deepseek';
protected function __construct()
{
$version = '1.1';
$version = '1.2';
$author = 'Christian Fasanando';
$message = 'Description';
@ -39,7 +40,7 @@ class AiHelperPlugin extends Plugin
}
/**
* Get the list of apis availables.
* Get the list of APIs available.
*
* @return array
*/
@ -47,20 +48,19 @@ class AiHelperPlugin extends Plugin
{
$list = [
self::OPENAI_API => 'OpenAI',
self::DEEPSEEK_API => 'DeepSeek',
];
return $list;
}
/**
* Get the completion text from openai.
* Get the completion text from the selected API.
*
* @return string
* @return string|array
*/
public function openAiGetCompletionText(
string $prompt,
string $toolName
) {
public function getCompletionText(string $prompt, string $toolName)
{
if (!$this->validateUserTokensLimit(api_get_user_id())) {
return [
'error' => true,
@ -68,49 +68,225 @@ class AiHelperPlugin extends Plugin
];
}
require_once __DIR__.'/src/openai/OpenAi.php';
$apiName = $this->get('api_name');
switch ($apiName) {
case self::OPENAI_API:
return $this->openAiGetCompletionText($prompt, $toolName);
case self::DEEPSEEK_API:
return $this->deepSeekGetCompletionText($prompt, $toolName);
default:
return [
'error' => true,
'message' => 'API not supported.',
];
}
}
/**
* Get completion text from OpenAI.
*/
public function openAiGetCompletionText(string $prompt, string $toolName)
{
try {
require_once __DIR__.'/src/openai/OpenAi.php';
$apiKey = $this->get('api_key');
$organizationId = $this->get('organization_id');
$ai = new OpenAi($apiKey, $organizationId);
$params = [
'model' => 'gpt-3.5-turbo-instruct',
'prompt' => $prompt,
'temperature' => 0.2,
'max_tokens' => 2000,
'frequency_penalty' => 0,
'presence_penalty' => 0.6,
'top_p' => 1.0,
];
$complete = $ai->completion($params);
$result = json_decode($complete, true);
if (isset($result['error'])) {
$errorMessage = $result['error']['message'] ?? 'Unknown error';
error_log("OpenAI Error: $errorMessage");
return [
'error' => true,
'message' => $errorMessage,
];
}
$resultText = $result['choices'][0]['text'] ?? '';
if (!empty($resultText)) {
$this->saveRequest([
'user_id' => api_get_user_id(),
'tool_name' => $toolName,
'prompt' => $prompt,
'prompt_tokens' => (int) ($result['usage']['prompt_tokens'] ?? 0),
'completion_tokens' => (int) ($result['usage']['completion_tokens'] ?? 0),
'total_tokens' => (int) ($result['usage']['total_tokens'] ?? 0),
]);
}
return $resultText ?: 'No response generated.';
} catch (Exception $e) {
return [
'error' => true,
'message' => 'An error occurred while connecting to OpenAI: ' . $e->getMessage(),
];
}
}
/**
* Get completion text from DeepSeek.
*/
public function deepSeekGetCompletionText(string $prompt, string $toolName)
{
$apiKey = $this->get('api_key');
$organizationId = $this->get('organization_id');
$ai = new OpenAi($apiKey, $organizationId);
$url = 'https://api.deepseek.com/chat/completions';
$temperature = 0.2;
$model = 'gpt-3.5-turbo-instruct';
$maxTokens = 2000;
$frequencyPenalty = 0;
$presencePenalty = 0.6;
$topP = 1.0;
$payload = [
'model' => 'deepseek-chat',
'messages' => [
[
'role' => 'system',
'content' => ($toolName === 'quiz')
? 'You are a helpful assistant that generates Aiken format questions.'
: 'You are a helpful assistant that generates learning path contents.',
],
[
'role' => 'user',
'content' => $prompt,
],
],
'stream' => false,
];
$complete = $ai->completion([
'model' => $model,
'prompt' => $prompt,
'temperature' => $temperature,
'max_tokens' => $maxTokens,
'frequency_penalty' => $frequencyPenalty,
'presence_penalty' => $presencePenalty,
'top_p' => $topP,
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: Bearer $apiKey",
]);
$result = json_decode($complete, true);
$resultText = '';
if (!empty($result['choices'])) {
$resultText = $result['choices'][0]['text'];
// saves information of user results.
$values = [
'user_id' => api_get_user_id(),
'tool_name' => $toolName,
'prompt' => $prompt,
'prompt_tokens' => (int) $result['usage']['prompt_tokens'],
'completion_tokens' => (int) $result['usage']['completion_tokens'],
'total_tokens' => (int) $result['usage']['total_tokens'],
$response = curl_exec($ch);
if ($response === false) {
error_log('cURL error: ' . curl_error($ch));
curl_close($ch);
return ['error' => true, 'message' => 'Request to AI provider failed.'];
}
curl_close($ch);
$result = json_decode($response, true);
if (isset($result['error'])) {
return [
'error' => true,
'message' => $result['error']['message'] ?? 'Unknown error',
];
$this->saveRequest($values);
}
$resultText = $result['choices'][0]['message']['content'] ?? '';
$this->saveRequest([
'user_id' => api_get_user_id(),
'tool_name' => $toolName,
'prompt' => $prompt,
'prompt_tokens' => 0,
'completion_tokens' => 0,
'total_tokens' => 0,
]);
return $resultText;
}
/**
* Generate questions based on the selected AI provider.
*
* @param int $nQ Number of questions
* @param string $lang Language for the questions
* @param string $topic Topic of the questions
* @param string $questionType Type of questions (e.g., 'multiple_choice')
* @return string Questions generated in Aiken format
* @throws Exception If an error occurs
*/
public function generateQuestions(int $nQ, string $lang, string $topic, string $questionType = 'multiple_choice'): string
{
$apiName = $this->get('api_name');
switch ($apiName) {
case self::OPENAI_API:
return $this->generateOpenAiQuestions($nQ, $lang, $topic, $questionType);
case self::DEEPSEEK_API:
return $this->generateDeepSeekQuestions($nQ, $lang, $topic, $questionType);
default:
throw new Exception("Unsupported API provider: $apiName");
}
}
/**
* Generate questions using OpenAI.
*/
private function generateOpenAiQuestions(int $nQ, string $lang, string $topic, string $questionType): string
{
$prompt = sprintf(
'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 line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. 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.',
$nQ,
$questionType,
$lang,
$topic
);
$result = $this->openAiGetCompletionText($prompt, 'quiz');
if (isset($result['error']) && true === $result['error']) {
throw new Exception($result['message']);
}
return $result;
}
/**
* Generate questions using DeepSeek.
*/
private function generateDeepSeekQuestions(int $nQ, string $lang, string $topic, string $questionType): string
{
$apiKey = $this->get('api_key');
$prompt = sprintf(
'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 line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. 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.',
$nQ,
$questionType,
$lang,
$topic
);
$payload = [
'model' => 'deepseek-chat',
'messages' => [
[
'role' => 'system',
'content' => 'You are a helpful assistant that generates Aiken format questions.',
],
[
'role' => 'user',
'content' => $prompt,
],
],
'stream' => false,
];
$deepSeek = new DeepSeek($apiKey);
$response = $deepSeek->generateQuestions($payload);
return $response;
}
/**
* Validates tokens limit of a user per current month.
*/

@ -1,46 +1,68 @@
AI Helper plugin
AI Helper Plugin
======
Version 1.1
Version 1.2
> This plugin is meant to be later integrated into Chamilo (in a major version
release).
> This plugin is designed to integrate AI functionality into Chamilo, providing tools for generating educational content, such as quizzes or learning paths, using AI providers like OpenAI or DeepSeek.
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:
### Overview
- exercises: in the Aiken import form, scrolling down
- learnpaths: option to create one with openai
The AI Helper plugin integrates into parts of the Chamilo platform that are most useful to teachers/trainers or learners. It allows pre-generating content, letting teachers/trainers review it before publishing.
### OpenAI/ChatGPT
Currently, this plugin is integrated into:
The plugin, created in early 2023, currently only supports OpenAI's ChatGPT API.
Create an account at https://platform.openai.com/signup (if you already have an API account, go
to https://platform.openai.com/login), then generate a secret key at https://platform.openai.com/account/api-keys
or click on "Personal" -> "View API keys".
Click the "Create new secret key" button, copy the key and use it to fill the "API key" field on the
plugin configuration page.
- **Exercises:** In the Aiken import form, with options to generate questions using OpenAI or DeepSeek.
- **Learnpaths:** Option to create structured learning paths with OpenAI or DeepSeek.
# Changelog
---
## v1.1
### Supported AI Providers
Added tracking for requests and differential settings to enable only in exercises, only in learning paths, or both.
#### OpenAI/ChatGPT
The plugin, created in early 2023, supports OpenAI's ChatGPT API.
- **Setup:**
1. Create an account at [OpenAI](https://platform.openai.com/signup) (or login if you already have one).
2. Generate a secret key at [API Keys](https://platform.openai.com/account/api-keys).
3. Click "Create new secret key", copy the key, and paste it into the "API key" field in the plugin configuration.
#### DeepSeek
DeepSeek is an alternative Open Source AI provider.
- **Setup:**
1. Create an account at [DeepSeek](https://www.deepseek.com/) (or login if you already have one).
2. Generate an API key at [API Keys](https://platform.deepseek.com/api_keys).
3. Click "Create new API key", copy the key, and paste it into the "API key" field in the plugin configuration.
---
### Features
- Generate quizzes in the Aiken format using AI.
- Create structured learning paths with AI assistance.
- Support for multiple AI providers, enabling easy switching between OpenAI and DeepSeek.
- Tracks API requests for monitoring usage and limits.
---
### Database Requirements
No additional database changes are required for v1.2.
The existing table `plugin_ai_helper_requests` is sufficient for tracking requests from both OpenAI and DeepSeek.
If you're updating from **v1.0**, ensure the following table exists:
To update from v1.0, execute the following queries manually.
```sql
CREATE TABLE plugin_ai_helper_requests (
id int(11) NOT NULL AUTO_INCREMENT,
user_id int(11) NOT NULL,
tool_name varchar(255) COLLATE utf8_unicode_ci NOT NULL,
requested_at datetime DEFAULT NULL,
request_text varchar(255) COLLATE utf8_unicode_ci NOT NULL,
prompt_tokens int(11) NOT NULL,
completion_tokens int(11) NOT NULL,
total_tokens int(11) NOT NULL,
PRIMARY KEY (id)
id int(11) NOT NULL AUTO_INCREMENT,
user_id int(11) NOT NULL,
tool_name varchar(255) COLLATE utf8_unicode_ci NOT NULL,
requested_at datetime DEFAULT NULL,
request_text varchar(255) COLLATE utf8_unicode_ci NOT NULL,
prompt_tokens int(11) NOT NULL,
completion_tokens int(11) NOT NULL,
total_tokens int(11) NOT NULL,
PRIMARY KEY (id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
```
If you got this update through Git, you will also need to run `composer install` to update the autoload mechanism.

@ -7,7 +7,9 @@ $strings['Description'] = 'Artificial Intelligence services (to use the broad te
$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['DeepSeek'] = 'DeepSeek';
$strings['api_key_help'] = 'The API key for the selected AI provider.';
$strings['api_name_help'] = 'Select the AI provider to use for question generation.';
$strings['organization_id'] = 'Organization ID';
$strings['organization_id_help'] = 'In case your api account is from an organization.';
$strings['OpenAI'] = 'OpenAI';

@ -16,3 +16,6 @@ $strings['tool_quiz_enable'] = "Activarlo en los ejercicios";
$strings['tokens_limit'] = "Limite de tokens IA";
$strings['tokens_limit_help'] = 'Limitar la cantidad máxima de tokens disponibles por usuario por mes para evitar un alto costo de servicio.';
$strings['ErrorTokensLimit'] = 'Lo sentimos, ha alcanzado la cantidad máxima de tokens o solicitudes configuradas por el administrador de la plataforma para el mes calendario actual. Comuníquese con su equipo de soporte o espere hasta el próximo mes antes de poder enviar nuevas solicitudes a AI Helper.';
$strings['DeepSeek'] = 'DeepSeek';
$strings['deepseek_api_key'] = 'Clave API de DeepSeek';
$strings['deepseek_api_key_help'] = 'Clave API para conectarse con DeepSeek.';

@ -0,0 +1,87 @@
<?php
/* For license terms, see /license.txt */
require_once 'Url.php';
class DeepSeek
{
private $apiKey;
private $headers;
public function __construct(string $apiKey)
{
$this->apiKey = $apiKey;
$this->headers = [
'Content-Type: application/json',
"Authorization: Bearer {$this->apiKey}",
];
}
/**
* Generate questions using the DeepSeek API.
*
* @param array $payload Data to send to the API
* @return string Decoded response from the API
* @throws Exception If an error occurs during the request
*/
public function generateQuestions(array $payload): string
{
$url = Url::completionsUrl();
$response = $this->sendRequest($url, 'POST', $payload);
if (empty($response)) {
throw new Exception('The DeepSeek API returned no response.');
}
$result = json_decode($response, true);
// Validate errors returned by the API
if (isset($result['error'])) {
throw new Exception("DeepSeek API Error: {$result['error']['message']}");
}
// Ensure the response contains the expected "choices" field
if (!isset($result['choices'][0]['message']['content'])) {
throw new Exception('Unexpected response format from the DeepSeek API.');
}
return $result['choices'][0]['message']['content'];
}
/**
* Send a request to the DeepSeek API.
*
* @param string $url Endpoint to send the request to
* @param string $method HTTP method (e.g., GET, POST)
* @param array $data Data to send as JSON
* @return string Raw response from the API
* @throws Exception If a cURL error occurs
*/
private function sendRequest(string $url, string $method, array $data = []): string
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$errorMessage = curl_error($ch);
curl_close($ch);
throw new Exception("cURL Error: {$errorMessage}");
}
curl_close($ch);
// Validate HTTP status codes
if ($httpCode < 200 || $httpCode >= 300) {
throw new Exception("Request to DeepSeek failed with HTTP status code: {$httpCode}");
}
return $response;
}
}

@ -0,0 +1,17 @@
<?php
/* For license terms, see /license.txt */
class Url
{
private const BASE_URL = 'https://api.deepseek.com/chat';
/**
* Get the endpoint URL for chat completions.
*
* @return string URL for the chat completions endpoint
*/
public static function completionsUrl(): string
{
return self::BASE_URL . '/completions';
}
}

@ -59,13 +59,9 @@ class OpenAi
*
* @return bool|string
*/
public function completion($opts, $stream = null)
public function completion(array $opts, callable $stream = null)
{
if ($stream != null && array_key_exists('stream', $opts)) {
if (!$opts['stream']) {
throw new Exception('Please provide a stream function.');
}
if ($stream !== null && isset($opts['stream']) && $opts['stream']) {
$this->streamMethod = $stream;
}
@ -287,14 +283,11 @@ class OpenAi
$this->timeout = $timeout;
}
private function sendRequest(
string $url,
string $method,
array $opts = []
) {
private function sendRequest(string $url, string $method, array $opts = []): string
{
$post_fields = json_encode($opts);
if (array_key_exists('file', $opts) || array_key_exists('image', $opts)) {
if (isset($opts['file']) || isset($opts['image'])) {
$this->headers[0] = $this->contentTypes["multipart/form-data"];
$post_fields = $opts;
} else {
@ -313,11 +306,11 @@ class OpenAi
CURLOPT_HTTPHEADER => $this->headers,
];
if ($opts == []) {
if (empty($opts)) {
unset($curl_info[CURLOPT_POSTFIELDS]);
}
if (array_key_exists('stream', $opts) && $opts['stream']) {
if (isset($opts['stream']) && $opts['stream']) {
$curl_info[CURLOPT_WRITEFUNCTION] = $this->streamMethod;
}
@ -325,8 +318,24 @@ class OpenAi
curl_setopt_array($curl, $curl_info);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if (curl_errno($curl)) {
$errorMessage = curl_error($curl);
curl_close($curl);
throw new Exception("cURL Error: {$errorMessage}");
}
curl_close($curl);
if ($httpCode === 429) {
throw new Exception("Insufficient quota. Please check your OpenAI account plan and billing details.");
}
if ($httpCode < 200 || $httpCode >= 300) {
throw new Exception("HTTP Error: {$httpCode}, Response: {$response}");
}
return $response;
}
}

@ -2,56 +2,30 @@
/* For license terms, see /license.txt */
/**
Answer questions based on existing knowledge.
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.");
try {
$nQ = (int) $_REQUEST['nro_questions'];
$lang = (string) $_REQUEST['language'];
$topic = (string) $_REQUEST['quiz_name'];
$questionType = $_REQUEST['question_type'] ?? 'multiple_choice';
$resultText = $plugin->generateQuestions($nQ, $lang, $topic, $questionType);
echo json_encode([
'success' => true,
'text' => trim($resultText),
]);
} catch (Exception $e) {
error_log("Error: " . $e->getMessage());
echo json_encode([
'success' => false,
'text' => $e->getMessage(),
]);
}
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 line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. 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.';
$prompt = sprintf($prompt, $nQ, $questionType, $lang, $topic);
$resultText = $plugin->openAiGetCompletionText($prompt, 'quiz');
if (isset($resultText['error']) && true === $resultText['error']) {
echo json_encode([
'success' => false,
'text' => $resultText['message'],
]);
exit;
}
// Returns the text answers generated.
$return = ['success' => false, 'text' => ''];
if (!empty($resultText)) {
$return = [
'success' => true,
'text' => trim($resultText),
];
}
echo json_encode($return);
break;
}

@ -18,199 +18,202 @@ $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.");
echo json_encode(['success' => false, 'text' => 'AI Provider not available.']);
exit;
}
switch ($apiName) {
case AiHelperPlugin::OPENAI_API:
$courseLanguage = (string) $_REQUEST['language'];
$chaptersCount = (int) $_REQUEST['nro_items'];
$topic = (string) $_REQUEST['lp_name'];
$wordsCount = (int) $_REQUEST['words_count'];
$courseCode = (string) $_REQUEST['course_code'];
$sessionId = (int) $_REQUEST['session_id'];
$addTests = ('true' === $_REQUEST['add_tests']);
$nQ = ($addTests ? (int) $_REQUEST['nro_questions'] : 0);
$messageGetItems = 'Generate the table of contents of a course in "%s" in %d or less chapters on the topic of "%s" and return it as a list of items separated by CRLF. Do not provide chapter numbering. Do not include a conclusion chapter.';
$prompt = sprintf($messageGetItems, $courseLanguage, $chaptersCount, $topic);
$resultText = $plugin->openAiGetCompletionText($prompt, 'learnpath');
if (isset($resultText['error']) && true === $resultText['error']) {
echo json_encode([
'success' => false,
'text' => $resultText['message'],
]);
exit;
}
$lpItems = [];
if (!empty($resultText)) {
$style = api_get_css_asset('bootstrap/dist/css/bootstrap.min.css');
$style .= api_get_css_asset('fontawesome/css/font-awesome.min.css');
$style .= api_get_css(ChamiloApi::getEditorDocStylePath());
$style .= api_get_css_asset('ckeditor/plugins/codesnippet/lib/highlight/styles/default.css');
$style .= api_get_asset('ckeditor/plugins/codesnippet/lib/highlight/highlight.pack.js');
$style .= '<script>hljs.initHighlightingOnLoad();</script>';
$items = explode("\n", $resultText);
$position = 1;
foreach ($items as $item) {
if (substr($item, 0, 2) === '- ') {
$item = substr($item, 2);
}
$explodedItem = preg_split('/\d\./', $item);
$title = count($explodedItem) > 1 ? $explodedItem[1] : $explodedItem[0];
if (!empty($title)) {
$lpItems[$position]['title'] = trim($title);
$messageGetItemContent = 'In the context of "%s", generate a document with HTML tags in "%s" with %d words of content or less, about "%s", as to be included as one chapter in a larger document on "%s". Consider the context is established for the reader and you do not need to repeat it.';
$promptItem = sprintf($messageGetItemContent, $topic, $courseLanguage, $wordsCount, $title, $topic);
$resultContentText = $plugin->openAiGetCompletionText($promptItem, 'learnpath');
$lpItemContent = (!empty($resultContentText) ? trim($resultContentText) : '');
if (false !== stripos($lpItemContent, '</head>')) {
$lpItemContent = preg_replace("|</head>|i", "\r\n$style\r\n\\0", $lpItemContent);
} else {
$lpItemContent = '<html><head><title>'.trim($title).'</title>'.$style.'</head><body>'.$lpItemContent.'</body></html>';
}
$lpItems[$position]['content'] = $lpItemContent;
$position++;
}
$courseLanguage = (string) $_REQUEST['language'];
$chaptersCount = (int) $_REQUEST['nro_items'];
$topic = (string) $_REQUEST['lp_name'];
$wordsCount = (int) $_REQUEST['words_count'];
$courseCode = (string) $_REQUEST['course_code'];
$sessionId = (int) $_REQUEST['session_id'];
$addTests = ('true' === $_REQUEST['add_tests']);
$nQ = ($addTests ? (int) $_REQUEST['nro_questions'] : 0);
$messageGetItems = 'Generate the table of contents of a course in "%s" in %d or fewer chapters on the topic "%s". Return it as a list of items separated by new lines. Do not include a conclusion chapter.';
$prompt = sprintf($messageGetItems, $courseLanguage, $chaptersCount, $topic);
$resultText = $plugin->getCompletionText($prompt, 'learnpath');
if (isset($resultText['error']) && $resultText['error']) {
echo json_encode(['success' => false, 'text' => $resultText['message']]);
exit;
}
if (empty($resultText)) {
echo json_encode(['success' => false, 'text' => 'AI returned no results.']);
exit;
}
$lpItems = [];
if (!empty($resultText)) {
$style = api_get_css_asset('bootstrap/dist/css/bootstrap.min.css');
$style .= api_get_css_asset('fontawesome/css/font-awesome.min.css');
$style .= api_get_css(ChamiloApi::getEditorDocStylePath());
$style .= api_get_css_asset('ckeditor/plugins/codesnippet/lib/highlight/styles/default.css');
$style .= api_get_asset('ckeditor/plugins/codesnippet/lib/highlight/highlight.pack.js');
$style .= '<script>hljs.initHighlightingOnLoad();</script>';
$items = explode("\n", $resultText);
$position = 1;
foreach ($items as $item) {
if (substr($item, 0, 2) === '- ') {
$item = substr($item, 2);
}
$explodedItem = preg_split('/\d\./', $item);
$title = count($explodedItem) > 1 ? $explodedItem[1] : $explodedItem[0];
if (!empty($title)) {
$lpItems[$position]['title'] = trim($title);
$messageGetItemContent = 'In the context of "%s", generate a document with HTML tags in "%s" with %d words of content or less, about "%s", as to be included as one chapter in a larger document on "%s". Consider the context is established for the reader and you do not need to repeat it.';
$promptItem = sprintf($messageGetItemContent, $topic, $courseLanguage, $wordsCount, $title, $topic);
$resultContentText = $plugin->getCompletionText($promptItem, 'learnpath');
if (isset($resultContentText['error']) && $resultContentText['error']) {
continue;
}
$lpItemContent = (!empty($resultContentText) ? trim($resultContentText) : '');
if (false !== stripos($lpItemContent, '</head>')) {
$lpItemContent = preg_replace("|</head>|i", "\r\n$style\r\n\\0", $lpItemContent);
} else {
$lpItemContent = '<html><head><title>'.trim($title).'</title>'.$style.'</head><body>'.$lpItemContent.'</body></html>';
}
$lpItems[$position]['content'] = $lpItemContent;
$position++;
}
}
}
// Create the learnpath and return the id generated.
$return = ['success' => false, 'lp_id' => 0];
if (!empty($lpItems)) {
$lpId = learnpath::add_lp(
$courseCode,
$topic,
'',
'chamilo',
'manual'
// Create the learnpath and return the id generated.
$return = ['success' => false, 'lp_id' => 0];
if (!empty($lpItems)) {
$lpId = learnpath::add_lp(
$courseCode,
$topic,
'',
'chamilo',
'manual'
);
if (!empty($lpId)) {
learnpath::toggle_visibility($lpId, 0);
$courseInfo = api_get_course_info($courseCode);
$lp = new \learnpath(
$courseCode,
$lpId,
api_get_user_id()
);
$lp->generate_lp_folder($courseInfo, $topic);
$order = 1;
$lpItemsIds = [];
foreach ($lpItems as $dspOrder => $item) {
$documentId = $lp->create_document(
$courseInfo,
$item['content'],
$item['title'],
'html'
);
if (!empty($lpId)) {
learnpath::toggle_visibility($lpId, 0);
$courseInfo = api_get_course_info($courseCode);
$lp = new \learnpath(
$courseCode,
$lpId,
api_get_user_id()
if (!empty($documentId)) {
$prevDocItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
$lpItemId = $lp->add_item(
0,
$prevDocItem,
'document',
$documentId,
$item['title'],
'',
0,
0,
api_get_user_id(),
$order
);
$lp->generate_lp_folder($courseInfo, $topic);
$order = 1;
$lpItemsIds = [];
foreach ($lpItems as $dspOrder => $item) {
$documentId = $lp->create_document(
$courseInfo,
$item['content'],
$item['title'],
'html'
);
if (!empty($documentId)) {
$prevDocItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
$lpItemId = $lp->add_item(
0,
$prevDocItem,
'document',
$documentId,
$item['title'],
'',
0,
0,
api_get_user_id(),
$order
);
$lpItemsIds[$order]['item_id'] = $lpItemId;
$lpItemsIds[$order]['item_type'] = 'document';
if ($addTests && !empty($lpItemId)) {
$promptQuiz = '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 line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. 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.';
$promptQuiz = sprintf($promptQuiz, $nQ, $courseLanguage, $item['title'], $topic);
$resultQuizText = $plugin->openAiGetCompletionText($promptQuiz, 'quiz');
if (!empty($resultQuizText)) {
$request = [];
$request['quiz_name'] = get_lang('Exercise').': '.$item['title'];
$request['nro_questions'] = $nQ;
$request['course_id'] = api_get_course_int_id($courseCode);
$request['aiken_format'] = trim($resultQuizText);
$exerciseId = aikenImportExercise(null, $request);
if (!empty($exerciseId)) {
$order++;
$prevQuizItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
$lpQuizItemId = $lp->add_item(
0,
$prevQuizItem,
'quiz',
$exerciseId,
$request['quiz_name'],
'',
0,
0,
api_get_user_id(),
$order
);
if (!empty($lpQuizItemId)) {
$maxScore = (float) $nQ;
$minScore = round($nQ / 2, 2);
$lpItemsIds[$order]['item_id'] = $lpQuizItemId;
$lpItemsIds[$order]['item_type'] = 'quiz';
$lpItemsIds[$order]['min_score'] = $minScore;
$lpItemsIds[$order]['max_score'] = $maxScore;
}
}
$lpItemsIds[$order]['item_id'] = $lpItemId;
$lpItemsIds[$order]['item_type'] = 'document';
if ($addTests && !empty($lpItemId)) {
$promptQuiz = '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 line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. 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.';
$promptQuiz = sprintf($promptQuiz, $nQ, $courseLanguage, $item['title'], $topic);
$resultQuizText = $plugin->getCompletionText($promptQuiz, 'quiz');
if (!empty($resultQuizText)) {
$request = [];
$request['quiz_name'] = get_lang('Exercise').': '.$item['title'];
$request['nro_questions'] = $nQ;
$request['course_id'] = api_get_course_int_id($courseCode);
$request['aiken_format'] = trim($resultQuizText);
$exerciseId = aikenImportExercise(null, $request);
if (!empty($exerciseId)) {
$order++;
$prevQuizItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
$lpQuizItemId = $lp->add_item(
0,
$prevQuizItem,
'quiz',
$exerciseId,
$request['quiz_name'],
'',
0,
0,
api_get_user_id(),
$order
);
if (!empty($lpQuizItemId)) {
$maxScore = (float) $nQ;
$minScore = round($nQ / 2, 2);
$lpItemsIds[$order]['item_id'] = $lpQuizItemId;
$lpItemsIds[$order]['item_type'] = 'quiz';
$lpItemsIds[$order]['min_score'] = $minScore;
$lpItemsIds[$order]['max_score'] = $maxScore;
}
}
}
$order++;
}
}
$order++;
}
// Add the final item
if ($addTests) {
$finalTitle = get_lang('EndOfLearningPath');
$finalContent = file_get_contents(api_get_path(SYS_CODE_PATH).'lp/final_item_template/template.html');
$finalDocId = $lp->create_document(
$courseInfo,
$finalContent,
$finalTitle
);
$prevFinalItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
$lpFinalItemId = $lp->add_item(
0,
$prevFinalItem,
TOOL_LP_FINAL_ITEM,
$finalDocId,
$finalTitle,
'',
0,
0,
api_get_user_id(),
$order
);
$lpItemsIds[$order]['item_id'] = $lpFinalItemId;
$lpItemsIds[$order]['item_type'] = TOOL_LP_FINAL_ITEM;
// Set lp items prerequisites
if (count($lpItemsIds) > 0) {
for ($i = 1; $i <= count($lpItemsIds); $i++) {
$prevIndex = ($i - 1);
if (isset($lpItemsIds[$prevIndex])) {
$itemId = $lpItemsIds[$i]['item_id'];
$prerequisite = $lpItemsIds[$prevIndex]['item_id'];
$minScore = ('quiz' === $lpItemsIds[$prevIndex]['item_type'] ? $lpItemsIds[$prevIndex]['min_score'] : 0);
$maxScore = ('quiz' === $lpItemsIds[$prevIndex]['item_type'] ? $lpItemsIds[$prevIndex]['max_score'] : 100);
$lp->edit_item_prereq($itemId, $prerequisite, $minScore, $maxScore);
}
}
// Add the final item
if ($addTests) {
$finalTitle = get_lang('EndOfLearningPath');
$finalContent = file_get_contents(api_get_path(SYS_CODE_PATH).'lp/final_item_template/template.html');
$finalDocId = $lp->create_document(
$courseInfo,
$finalContent,
$finalTitle
);
$prevFinalItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
$lpFinalItemId = $lp->add_item(
0,
$prevFinalItem,
TOOL_LP_FINAL_ITEM,
$finalDocId,
$finalTitle,
'',
0,
0,
api_get_user_id(),
$order
);
$lpItemsIds[$order]['item_id'] = $lpFinalItemId;
$lpItemsIds[$order]['item_type'] = TOOL_LP_FINAL_ITEM;
// Set lp items prerequisites
if (count($lpItemsIds) > 0) {
for ($i = 1; $i <= count($lpItemsIds); $i++) {
$prevIndex = ($i - 1);
if (isset($lpItemsIds[$prevIndex])) {
$itemId = $lpItemsIds[$i]['item_id'];
$prerequisite = $lpItemsIds[$prevIndex]['item_id'];
$minScore = ('quiz' === $lpItemsIds[$prevIndex]['item_type'] ? $lpItemsIds[$prevIndex]['min_score'] : 0);
$maxScore = ('quiz' === $lpItemsIds[$prevIndex]['item_type'] ? $lpItemsIds[$prevIndex]['max_score'] : 100);
$lp->edit_item_prereq($itemId, $prerequisite, $minScore, $maxScore);
}
}
}
$return = [
'success' => true,
'lp_id' => $lpId,
];
}
echo json_encode($return);
break;
}
$return = [
'success' => true,
'lp_id' => $lpId,
];
}
echo json_encode($return);

Loading…
Cancel
Save