diff --git a/main/exercise/export/aiken/aiken_import.inc.php b/main/exercise/export/aiken/aiken_import.inc.php
index c18a0cc26a2..053fce3c5c3 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('
'
+ . sprintf(get_lang('UsingAIProviderX'), ''.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 ad1bcadc731..9d56ea510e7 100644
--- a/main/lp/LpAiHelper.php
+++ b/main/lp/LpAiHelper.php
@@ -19,6 +19,11 @@ public function __construct()
*/
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 @@ public function aiHelperForm()
null
);
$form->addElement('header', get_lang('LpAiGenerator'));
+
+ if ($hasSingleApi) {
+ $apiName = $availableApis[$configuredApi] ?? $configuredApi;
+ $form->addHtml(''
+ .sprintf(get_lang('UsingAIProviderX'), ''.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 @@ public function aiHelperForm()
$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 cc546430bed..2b50655d4a7 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 @@ protected function __construct()
}
/**
- * Get the list of apis availables.
+ * Get the list of APIs available.
*
* @return array
*/
@@ -47,20 +48,19 @@ public function getApiList()
{
$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 @@ public function openAiGetCompletionText(
];
}
- 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.
*/
diff --git a/plugin/ai_helper/README.md b/plugin/ai_helper/README.md
index 9e18f646172..fb245d8b86b 100755
--- a/plugin/ai_helper/README.md
+++ b/plugin/ai_helper/README.md
@@ -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.
diff --git a/plugin/ai_helper/lang/english.php b/plugin/ai_helper/lang/english.php
index 17bd047420e..36ede273f74 100755
--- a/plugin/ai_helper/lang/english.php
+++ b/plugin/ai_helper/lang/english.php
@@ -7,7 +7,9 @@
$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 0ff50eb51de..f78c97814a0 100755
--- a/plugin/ai_helper/lang/spanish.php
+++ b/plugin/ai_helper/lang/spanish.php
@@ -16,3 +16,6 @@
$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 00000000000..7c0f2eacdaf
--- /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 00000000000..bfd1e2c8226
--- /dev/null
+++ b/plugin/ai_helper/src/deepseek/Url.php
@@ -0,0 +1,17 @@
+streamMethod = $stream;
}
@@ -287,14 +283,11 @@ public function setTimeout(int $timeout)
$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 @@ private function sendRequest(
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 @@ private function sendRequest(
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 4ee9018dea8..97263c355c5 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 b8fb74294d4..7a49e104791 100644
--- a/plugin/ai_helper/tool/learnpath.php
+++ b/plugin/ai_helper/tool/learnpath.php
@@ -18,199 +18,202 @@
$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);