From a9d0097466e5112d7fbf592a15247ce5bb44a9ae Mon Sep 17 00:00:00 2001
From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com>
Date: Fri, 17 Jan 2025 11:32:49 -0500
Subject: [PATCH 01/11] Avoid double htmlspecialchars for links using
Display::url
See 15023ce6301a8b623d789e8e788b2db9bf470547
---
main/inc/lib/display.lib.php | 1 -
1 file changed, 1 deletion(-)
diff --git a/main/inc/lib/display.lib.php b/main/inc/lib/display.lib.php
index 3b9e8910b4..74b05556b3 100755
--- a/main/inc/lib/display.lib.php
+++ b/main/inc/lib/display.lib.php
@@ -946,7 +946,6 @@ class Display
{
if (!empty($url)) {
$url = preg_replace('#&#', '&', $url);
- $url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$attributes['href'] = $url;
}
From 6954d866c7513bcad937215f9061232ca11e8739 Mon Sep 17 00:00:00 2001
From: Christian Beeznest
Date: Wed, 22 Jan 2025 15:47:18 -0500
Subject: [PATCH 02/11] Plugin: Add DeepSeek support and adapt AI Helper for
content and quiz generation
---
.../export/aiken/aiken_import.inc.php | 47 ++-
main/lp/LpAiHelper.php | 118 +++---
plugin/ai_helper/AiHelperPlugin.php | 249 ++++++++++--
plugin/ai_helper/README.md | 79 ++--
plugin/ai_helper/lang/english.php | 4 +-
plugin/ai_helper/lang/spanish.php | 3 +
plugin/ai_helper/src/deepseek/DeepSeek.php | 87 +++++
plugin/ai_helper/src/deepseek/Url.php | 17 +
plugin/ai_helper/src/openai/OpenAi.php | 37 +-
plugin/ai_helper/tool/answers.php | 64 +---
plugin/ai_helper/tool/learnpath.php | 357 +++++++++---------
11 files changed, 678 insertions(+), 384 deletions(-)
create mode 100644 plugin/ai_helper/src/deepseek/DeepSeek.php
create mode 100644 plugin/ai_helper/src/deepseek/Url.php
diff --git a/main/exercise/export/aiken/aiken_import.inc.php b/main/exercise/export/aiken/aiken_import.inc.php
index c18a0cc26a..48d38e7f8f 100755
--- a/main/exercise/export/aiken/aiken_import.inc.php
+++ b/main/exercise/export/aiken/aiken_import.inc.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(''
+ . get_lang('UsingAIProvider') . ': ' . htmlspecialchars($apiName) . '
');
+ }
+
+ $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').'");
diff --git a/main/lp/LpAiHelper.php b/main/lp/LpAiHelper.php
index ad1bcadc73..7741bdcf8b 100644
--- a/main/lp/LpAiHelper.php
+++ b/main/lp/LpAiHelper.php
@@ -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(''
+ . get_lang('UsingAIProvider') . ': ' . htmlspecialchars($apiName) . '
');
+ }
+
$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('');
-
- $form->addButton(
- 'create_lp_button',
- get_lang('LearnpathAddLearnpath'),
- '',
- 'default',
- 'default',
- null,
- ['id' => 'create-lp-ai']
- );
+ }
+ });
+ });
+ ');
+ $form->addButton('create_lp_button', get_lang('LearnpathAddLearnpath'), '', 'default', 'default', null, ['id' => 'create-lp-ai']);
echo $form->returnForm();
}
}
diff --git a/plugin/ai_helper/AiHelperPlugin.php b/plugin/ai_helper/AiHelperPlugin.php
index cc546430be..9eecde48aa 100644
--- a/plugin/ai_helper/AiHelperPlugin.php
+++ b/plugin/ai_helper/AiHelperPlugin.php
@@ -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
+ * @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,218 @@ 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('Error en cURL: ' . 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);
+ 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
+ {
+ $apiKey = $this->get('api_key');
+ $payload = [
+ 'model' => 'deepseek-chat',
+ 'messages' => [
+ [
+ 'role' => 'system',
+ 'content' => 'You are a helpful assistant that generates Aiken format questions.',
+ ],
+ [
+ 'role' => 'user',
+ 'content' => "Generate $nQ multiple choice questions about \"$topic\" in $lang.",
+ ],
+ ],
+ 'stream' => false,
+ ];
+
+ $deepSeek = new DeepSeek($apiKey);
+ $response = $deepSeek->generateQuestions($payload);
+
+ return $response;
+ }
+
/**
* Validates tokens limit of a user per current month.
*/
diff --git a/plugin/ai_helper/README.md b/plugin/ai_helper/README.md
index 9e18f64617..0f96c41327 100755
--- a/plugin/ai_helper/README.md
+++ b/plugin/ai_helper/README.md
@@ -1,46 +1,67 @@
-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 AI provider focused on generating educational content.
+- **Setup:**
+1. Obtain an API key from your DeepSeek account.
+2. Paste the key 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.
diff --git a/plugin/ai_helper/lang/english.php b/plugin/ai_helper/lang/english.php
index 17bd047420..36ede273f7 100755
--- a/plugin/ai_helper/lang/english.php
+++ b/plugin/ai_helper/lang/english.php
@@ -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';
diff --git a/plugin/ai_helper/lang/spanish.php b/plugin/ai_helper/lang/spanish.php
index 0ff50eb51d..f78c97814a 100755
--- a/plugin/ai_helper/lang/spanish.php
+++ b/plugin/ai_helper/lang/spanish.php
@@ -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.';
diff --git a/plugin/ai_helper/src/deepseek/DeepSeek.php b/plugin/ai_helper/src/deepseek/DeepSeek.php
new file mode 100644
index 0000000000..7c0f2eacda
--- /dev/null
+++ b/plugin/ai_helper/src/deepseek/DeepSeek.php
@@ -0,0 +1,87 @@
+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;
+ }
+}
diff --git a/plugin/ai_helper/src/deepseek/Url.php b/plugin/ai_helper/src/deepseek/Url.php
new file mode 100644
index 0000000000..bfd1e2c822
--- /dev/null
+++ b/plugin/ai_helper/src/deepseek/Url.php
@@ -0,0 +1,17 @@
+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;
}
}
diff --git a/plugin/ai_helper/tool/answers.php b/plugin/ai_helper/tool/answers.php
index 4ee9018dea..97263c355c 100644
--- a/plugin/ai_helper/tool/answers.php
+++ b/plugin/ai_helper/tool/answers.php
@@ -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;
-}
diff --git a/plugin/ai_helper/tool/learnpath.php b/plugin/ai_helper/tool/learnpath.php
index b8fb74294d..7a49e10479 100644
--- a/plugin/ai_helper/tool/learnpath.php
+++ b/plugin/ai_helper/tool/learnpath.php
@@ -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 .= '';
-
- $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, '')) {
- $lpItemContent = preg_replace("||i", "\r\n$style\r\n\\0", $lpItemContent);
- } else {
- $lpItemContent = ''.trim($title).''.$style.''.$lpItemContent.'';
- }
- $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 .= '';
+
+ $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, '')) {
+ $lpItemContent = preg_replace("||i", "\r\n$style\r\n\\0", $lpItemContent);
+ } else {
+ $lpItemContent = ''.trim($title).''.$style.''.$lpItemContent.'';
}
+ $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);
From ba277d474cb7466e3fc9b7d9c1efdf216fd201ac Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Wed, 22 Jan 2025 23:49:13 +0100
Subject: [PATCH 03/11] Minor: Plugin: AI Helper: Use placeholder in language
string
---
main/exercise/export/aiken/aiken_import.inc.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/main/exercise/export/aiken/aiken_import.inc.php b/main/exercise/export/aiken/aiken_import.inc.php
index 48d38e7f8f..94a8769c68 100755
--- a/main/exercise/export/aiken/aiken_import.inc.php
+++ b/main/exercise/export/aiken/aiken_import.inc.php
@@ -70,7 +70,7 @@ function generateAikenForm()
if ($hasSingleApi) {
$apiName = $availableApis[$configuredApi] ?? $configuredApi;
$form->addHtml(''
- . get_lang('UsingAIProvider') . ': ' . htmlspecialchars($apiName) . '
');
+ . sprintf(get_lang('UsingAIProviderX'), ': '.htmlspecialchars($apiName).'').'');
}
$form->addElement('text', 'quiz_name', get_lang('QuestionsTopic'));
From e9c2ddae5848b0afcc35029ed06249f2efb7741a Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Wed, 22 Jan 2025 23:54:45 +0100
Subject: [PATCH 04/11] Minor: Plugin: AI Helper: Use placeholder in language
string
---
main/lp/LpAiHelper.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/main/lp/LpAiHelper.php b/main/lp/LpAiHelper.php
index 7741bdcf8b..9d56ea510e 100644
--- a/main/lp/LpAiHelper.php
+++ b/main/lp/LpAiHelper.php
@@ -35,7 +35,7 @@ class LpAiHelper
if ($hasSingleApi) {
$apiName = $availableApis[$configuredApi] ?? $configuredApi;
$form->addHtml(''
- . get_lang('UsingAIProvider') . ': ' . htmlspecialchars($apiName) . '
');
+ .sprintf(get_lang('UsingAIProviderX'), ''.htmlspecialchars($apiName).'').'');
}
$form->addElement('text', 'lp_name', [get_lang('LpAiTopic'), get_lang('LpAiTopicHelp')]);
From a2d6988152a54f7cb5fcdb39845268043dcf3198 Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Wed, 22 Jan 2025 23:55:34 +0100
Subject: [PATCH 05/11] Minor: Plugin: AI Helper: Use placeholder in language
string
---
main/exercise/export/aiken/aiken_import.inc.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/main/exercise/export/aiken/aiken_import.inc.php b/main/exercise/export/aiken/aiken_import.inc.php
index 94a8769c68..053fce3c5c 100755
--- a/main/exercise/export/aiken/aiken_import.inc.php
+++ b/main/exercise/export/aiken/aiken_import.inc.php
@@ -70,7 +70,7 @@ function generateAikenForm()
if ($hasSingleApi) {
$apiName = $availableApis[$configuredApi] ?? $configuredApi;
$form->addHtml(''
- . sprintf(get_lang('UsingAIProviderX'), ': '.htmlspecialchars($apiName).'').'
');
+ . sprintf(get_lang('UsingAIProviderX'), ''.htmlspecialchars($apiName).'').'');
}
$form->addElement('text', 'quiz_name', get_lang('QuestionsTopic'));
From fe4564802c7a35c1ec5c6ef8381610c3d12e0a6b Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Thu, 23 Jan 2025 00:03:14 +0100
Subject: [PATCH 06/11] Plugin: AI Helper: Fix the DeepSeek prompt (use same as
for OpenAI by default)
---
plugin/ai_helper/AiHelperPlugin.php | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/plugin/ai_helper/AiHelperPlugin.php b/plugin/ai_helper/AiHelperPlugin.php
index 9eecde48aa..4719bee1b5 100644
--- a/plugin/ai_helper/AiHelperPlugin.php
+++ b/plugin/ai_helper/AiHelperPlugin.php
@@ -259,6 +259,13 @@ class AiHelperPlugin extends Plugin
private function generateDeepSeekQuestions(int $nQ, string $lang, string $topic): 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' => [
@@ -268,7 +275,7 @@ class AiHelperPlugin extends Plugin
],
[
'role' => 'user',
- 'content' => "Generate $nQ multiple choice questions about \"$topic\" in $lang.",
+ 'content' => $prompt,
],
],
'stream' => false,
From 2c2b93e83e59e9951adba9687615ca7afc666d36 Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Thu, 23 Jan 2025 00:31:27 +0100
Subject: [PATCH 07/11] Minor: Plugin: AI Helper: Fix error message language
---
plugin/ai_helper/AiHelperPlugin.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/ai_helper/AiHelperPlugin.php b/plugin/ai_helper/AiHelperPlugin.php
index 4719bee1b5..d6d262804e 100644
--- a/plugin/ai_helper/AiHelperPlugin.php
+++ b/plugin/ai_helper/AiHelperPlugin.php
@@ -179,7 +179,7 @@ class AiHelperPlugin extends Plugin
$response = curl_exec($ch);
if ($response === false) {
- error_log('Error en cURL: ' . curl_error($ch));
+ error_log('cURL error: ' . curl_error($ch));
curl_close($ch);
return ['error' => true, 'message' => 'Request to AI provider failed.'];
}
From 57b0fe66a2ac5abf1e9baf2597ec9125d4294abe Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Thu, 23 Jan 2025 00:42:36 +0100
Subject: [PATCH 08/11] Plugin: AI Helper: Fix params of
generateDeepSeekQuestions()
---
plugin/ai_helper/AiHelperPlugin.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugin/ai_helper/AiHelperPlugin.php b/plugin/ai_helper/AiHelperPlugin.php
index d6d262804e..2b50655d4a 100644
--- a/plugin/ai_helper/AiHelperPlugin.php
+++ b/plugin/ai_helper/AiHelperPlugin.php
@@ -226,7 +226,7 @@ class AiHelperPlugin extends Plugin
case self::OPENAI_API:
return $this->generateOpenAiQuestions($nQ, $lang, $topic, $questionType);
case self::DEEPSEEK_API:
- return $this->generateDeepSeekQuestions($nQ, $lang, $topic);
+ return $this->generateDeepSeekQuestions($nQ, $lang, $topic, $questionType);
default:
throw new Exception("Unsupported API provider: $apiName");
}
@@ -256,7 +256,7 @@ class AiHelperPlugin extends Plugin
/**
* Generate questions using DeepSeek.
*/
- private function generateDeepSeekQuestions(int $nQ, string $lang, string $topic): string
+ private function generateDeepSeekQuestions(int $nQ, string $lang, string $topic, string $questionType): string
{
$apiKey = $this->get('api_key');
$prompt = sprintf(
From e06aac8494ff1ecc4462ab5d06c858e4163dc688 Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Thu, 23 Jan 2025 00:53:49 +0100
Subject: [PATCH 09/11] Plugin: AI Helper: Update README
---
plugin/ai_helper/README.md | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/plugin/ai_helper/README.md b/plugin/ai_helper/README.md
index 0f96c41327..fb245d8b86 100755
--- a/plugin/ai_helper/README.md
+++ b/plugin/ai_helper/README.md
@@ -25,13 +25,14 @@ 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.
+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 AI provider focused on generating educational content.
+DeepSeek is an alternative Open Source AI provider.
- **Setup:**
-1. Obtain an API key from your DeepSeek account.
-2. Paste the key into the "API key" field in the plugin configuration.
+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.
---
From aceb9092c217d42e65bf4c74007eb4ddb51fdb3f Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Thu, 23 Jan 2025 09:47:32 +0100
Subject: [PATCH 10/11] Language: Update language terms
---
main/lang/english/trad4all.inc.php | 2 ++
main/lang/french/trad4all.inc.php | 2 ++
main/lang/spanish/trad4all.inc.php | 2 ++
3 files changed, 6 insertions(+)
diff --git a/main/lang/english/trad4all.inc.php b/main/lang/english/trad4all.inc.php
index 5302fdbbe1..496af339d2 100644
--- a/main/lang/english/trad4all.inc.php
+++ b/main/lang/english/trad4all.inc.php
@@ -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";
?>
\ No newline at end of file
diff --git a/main/lang/french/trad4all.inc.php b/main/lang/french/trad4all.inc.php
index 812907dae3..c4dea73bc4 100644
--- a/main/lang/french/trad4all.inc.php
+++ b/main/lang/french/trad4all.inc.php
@@ -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";
?>
\ No newline at end of file
diff --git a/main/lang/spanish/trad4all.inc.php b/main/lang/spanish/trad4all.inc.php
index d478d8824f..5fcf4dce5b 100644
--- a/main/lang/spanish/trad4all.inc.php
+++ b/main/lang/spanish/trad4all.inc.php
@@ -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";
?>
\ No newline at end of file
From b1a3f971e967d0f771e0d7adf8cb9403d76c85f1 Mon Sep 17 00:00:00 2001
From: Yannick Warnier
Date: Thu, 23 Jan 2025 09:50:22 +0100
Subject: [PATCH 11/11] Language: Update language terms - Fix Dutch
---
main/lang/dutch/trad4all.inc.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/main/lang/dutch/trad4all.inc.php b/main/lang/dutch/trad4all.inc.php
index f9dc359444..c9e5ec5e55 100644
--- a/main/lang/dutch/trad4all.inc.php
+++ b/main/lang/dutch/trad4all.inc.php
@@ -5145,7 +5145,7 @@ $DescriptionContent = "Deze module zal u helpen uw cursus op een gemakkelijke
$LinksContent = "
De linkmodule laat u toe een verzameling cursusmateriaal aan te leggen voor de studenten, vooral materiaal dat u niet zelf hebt aangemaakt.
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).
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.
";
$MycoursesContent = "Eenmaal ingelogd bent u op uw persoonlijke startpagina.
In het hoofdgedeelte (centrum) ziet u \"Mijn cursussen\", 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).
Bovenaan ziet u
\t- \"Mijn profiel\": hier kunt u verschillende zaken (naam, voornaam, e-mailadres…) wijzigen. U kunt ook de statistieken van uw platformgebruik opvragen.
\t- \"Mijn agenda\": dit bevat de agenda-items van al uw cursussen.
Wijzig mijn cursuslijst, in het menu aan de rechterkant, laat u toe in te schrijven voor nieuwe cursussen. Deze link laat u ook uitschrijven uit cursussen.
De links Ondersteuningsforum en Documentatie verwijzen naar de Chamilowebsite, waar u terecht kunt voor technische informatie of pedagogisch advies.
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.
";
$AgendaContent = "De agenda verschijnt niet alleen in elke cursus zelf, maar ook als een overzichtsagenda voor de student ('Mijn agenda' in de menubalk bovenaan).
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.
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.
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.
";
-$AnnouncementsContent = "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.
Kies 'Aankondiging toevoegen' en selecteer groepen/gebruikers. Gebruik Ctrl +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.
";
+$AnnouncementsContent = "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.
Kies 'Aankondiging toevoegen' en selecteer groepen/gebruikers. Gebruik Ctrl +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.
";
$ChatContent = "In deze chatbox kunt u live met uw studenten discussiëren.
Deze chatfunctie werkt anders dan IM-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.
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.
Alleen de cursusbeheerder kan de discussie verwijderen wanneer hij/zij dit verkiest.
Pedagogische relevantieEen 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.
";
$WorkContent = "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.
U kan kiezen of door de studenten geplaatste documenten standaard zichtbaar of onzichtbaar zijn.
Deze module heeft een Introductietekstgebied waarin u een vraag, richtlijnen, deadlines of andere zaken kan noteren.
";
$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 SCORM-cursussen gebruikt, kan u zelfs nagaan hoeveel tijd een student gespendeerd heeft aan een module of hoofdstuk. De statistieken leveren informatie op twee niveaus:\t- Globaal: Hoeveel studenten bezochten de cursus? Wat zijn de meest bezochte pagina's en links?
\t- Nominatief: Welke pagina heeft Jan Janssens bezocht? Welke score behaalde hij op zijn tests? Wanneer heeft hij laatst ingelogd op het systeem?
";
@@ -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.";
-?>
\ No newline at end of file
+?>