upgrade
This commit is contained in:
421
plugin/ai_helper/AiHelperPlugin.php
Normal file
421
plugin/ai_helper/AiHelperPlugin.php
Normal file
@@ -0,0 +1,421 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
use Chamilo\PluginBundle\Entity\AiHelper\Requests;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
|
||||
require_once __DIR__.'/src/deepseek/DeepSeek.php';
|
||||
/**
|
||||
* Description of AiHelperPlugin.
|
||||
*
|
||||
* @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.2';
|
||||
$author = 'Christian Fasanando';
|
||||
|
||||
$message = 'Description';
|
||||
|
||||
$settings = [
|
||||
$message => 'html',
|
||||
'tool_enable' => 'boolean',
|
||||
'api_name' => [
|
||||
'type' => 'select',
|
||||
'options' => $this->getApiList(),
|
||||
],
|
||||
'api_key' => 'text',
|
||||
'organization_id' => 'text',
|
||||
'tool_lp_enable' => 'boolean',
|
||||
'tool_quiz_enable' => 'boolean',
|
||||
'tokens_limit' => 'text',
|
||||
];
|
||||
|
||||
parent::__construct($version, $author, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of APIs available.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getApiList()
|
||||
{
|
||||
$list = [
|
||||
self::OPENAI_API => 'OpenAI',
|
||||
self::DEEPSEEK_API => 'DeepSeek',
|
||||
];
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the completion text from the selected API.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
public function getCompletionText(string $prompt, string $toolName)
|
||||
{
|
||||
if (!$this->validateUserTokensLimit(api_get_user_id())) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => $this->get_lang('ErrorTokensLimit'),
|
||||
];
|
||||
}
|
||||
|
||||
$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');
|
||||
|
||||
$url = 'https://api.deepseek.com/chat/completions';
|
||||
|
||||
$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,
|
||||
];
|
||||
|
||||
$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",
|
||||
]);
|
||||
|
||||
$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',
|
||||
];
|
||||
}
|
||||
|
||||
$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')
|
||||
*
|
||||
* @throws Exception If an error occurs
|
||||
*
|
||||
* @return string Questions generated in Aiken format
|
||||
*/
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates tokens limit of a user per current month.
|
||||
*/
|
||||
public function validateUserTokensLimit(int $userId): bool
|
||||
{
|
||||
$em = Database::getManager();
|
||||
$repo = $em->getRepository('ChamiloPluginBundle:AiHelper\Requests');
|
||||
|
||||
$startDate = api_get_utc_datetime(
|
||||
null,
|
||||
false,
|
||||
true)
|
||||
->modify('first day of this month')->setTime(00, 00, 00)
|
||||
;
|
||||
$endDate = api_get_utc_datetime(
|
||||
null,
|
||||
false,
|
||||
true)
|
||||
->modify('last day of this month')->setTime(23, 59, 59)
|
||||
;
|
||||
|
||||
$qb = $repo->createQueryBuilder('e')
|
||||
->select('sum(e.totalTokens) as total')
|
||||
->andWhere('e.requestedAt BETWEEN :dateMin AND :dateMax')
|
||||
->andWhere('e.userId = :user')
|
||||
->setMaxResults(1)
|
||||
->setParameters(
|
||||
[
|
||||
'dateMin' => $startDate->format('Y-m-d h:i:s'),
|
||||
'dateMax' => $endDate->format('Y-m-d h:i:s'),
|
||||
'user' => $userId,
|
||||
]
|
||||
);
|
||||
$result = $qb->getQuery()->getOneOrNullResult();
|
||||
$totalTokens = !empty($result) ? (int) $result['total'] : 0;
|
||||
|
||||
$valid = true;
|
||||
$tokensLimit = $this->get('tokens_limit');
|
||||
if (!empty($tokensLimit)) {
|
||||
$valid = ($totalTokens <= (int) $tokensLimit);
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin directory name.
|
||||
*/
|
||||
public function get_name(): string
|
||||
{
|
||||
return 'ai_helper';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class instance.
|
||||
*
|
||||
* @staticvar AiHelperPlugin $result
|
||||
*/
|
||||
public static function create(): AiHelperPlugin
|
||||
{
|
||||
static $result = null;
|
||||
|
||||
return $result ?: $result = new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user information of openai request.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function saveRequest(array $values)
|
||||
{
|
||||
$em = Database::getManager();
|
||||
|
||||
$objRequest = new Requests();
|
||||
$objRequest
|
||||
->setUserId($values['user_id'])
|
||||
->setToolName($values['tool_name'])
|
||||
->setRequestedAt(new DateTime())
|
||||
->setRequestText($values['prompt'])
|
||||
->setPromptTokens($values['prompt_tokens'])
|
||||
->setCompletionTokens($values['completion_tokens'])
|
||||
->setTotalTokens($values['total_tokens'])
|
||||
;
|
||||
$em->persist($objRequest);
|
||||
$em->flush();
|
||||
|
||||
return $objRequest->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the plugin. Set the database up.
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$em = Database::getManager();
|
||||
|
||||
if ($em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_REQUESTS])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schemaTool = new SchemaTool($em);
|
||||
$schemaTool->createSchema(
|
||||
[
|
||||
$em->getClassMetadata(Requests::class),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unistall plugin. Clear the database.
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
$em = Database::getManager();
|
||||
|
||||
if (!$em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_REQUESTS])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schemaTool = new SchemaTool($em);
|
||||
$schemaTool->dropSchema(
|
||||
[
|
||||
$em->getClassMetadata(Requests::class),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
171
plugin/ai_helper/Entity/Requests.php
Normal file
171
plugin/ai_helper/Entity/Requests.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
namespace Chamilo\PluginBundle\Entity\AiHelper;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Class Platform.
|
||||
*
|
||||
* @package Chamilo\PluginBundle\Entity\AiHelper
|
||||
*
|
||||
* @ORM\Table(name="plugin_ai_helper_requests")
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class Requests
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id()
|
||||
* @ORM\GeneratedValue()
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="user_id", type="integer")
|
||||
*/
|
||||
private $userId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="tool_name", type="string")
|
||||
*/
|
||||
private $toolName;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="requested_at", type="datetime", nullable=true)
|
||||
*/
|
||||
private $requestedAt;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="request_text", type="string")
|
||||
*/
|
||||
private $requestText;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="prompt_tokens", type="integer")
|
||||
*/
|
||||
private $promptTokens;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="completion_tokens", type="integer")
|
||||
*/
|
||||
private $completionTokens;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="total_tokens", type="integer")
|
||||
*/
|
||||
private $totalTokens;
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): Requests
|
||||
{
|
||||
$this->userId = $userId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(int $id): Requests
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRequestedAt(): \DateTime
|
||||
{
|
||||
return $this->requestedAt;
|
||||
}
|
||||
|
||||
public function setRequestedAt(\DateTime $requestedAt): Requests
|
||||
{
|
||||
$this->requestedAt = $requestedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRequestText(): string
|
||||
{
|
||||
return $this->requestText;
|
||||
}
|
||||
|
||||
public function setRequestText(string $requestText): Requests
|
||||
{
|
||||
$this->requestText = $requestText;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPromptTokens(): int
|
||||
{
|
||||
return $this->promptTokens;
|
||||
}
|
||||
|
||||
public function setPromptTokens(int $promptTokens): Requests
|
||||
{
|
||||
$this->promptTokens = $promptTokens;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCompletionTokens(): int
|
||||
{
|
||||
return $this->completionTokens;
|
||||
}
|
||||
|
||||
public function setCompletionTokens(int $completionTokens): Requests
|
||||
{
|
||||
$this->completionTokens = $completionTokens;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotalTokens(): int
|
||||
{
|
||||
return $this->totalTokens;
|
||||
}
|
||||
|
||||
public function setTotalTokens(int $totalTokens): Requests
|
||||
{
|
||||
$this->totalTokens = $totalTokens;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getToolName(): string
|
||||
{
|
||||
return $this->toolName;
|
||||
}
|
||||
|
||||
public function setToolName(string $toolName): Requests
|
||||
{
|
||||
$this->toolName = $toolName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
68
plugin/ai_helper/README.md
Normal file
68
plugin/ai_helper/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
AI Helper Plugin
|
||||
======
|
||||
|
||||
Version 1.2
|
||||
|
||||
> 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.
|
||||
|
||||
---
|
||||
|
||||
### Overview
|
||||
|
||||
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.
|
||||
|
||||
Currently, this plugin is integrated into:
|
||||
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
### Supported AI Providers
|
||||
|
||||
#### 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:
|
||||
|
||||
```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)
|
||||
) 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.
|
||||
16
plugin/ai_helper/install.php
Normal file
16
plugin/ai_helper/install.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Install the Ai Helper Plugin.
|
||||
*
|
||||
* @package chamilo.plugin.ai_helper
|
||||
*/
|
||||
require_once __DIR__.'/../../main/inc/global.inc.php';
|
||||
require_once __DIR__.'/AiHelperPlugin.php';
|
||||
|
||||
if (!api_is_platform_admin()) {
|
||||
exit('You must have admin permissions to install plugins');
|
||||
}
|
||||
|
||||
AiHelperPlugin::create()->install();
|
||||
20
plugin/ai_helper/lang/english.php
Normal file
20
plugin/ai_helper/lang/english.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
$strings['plugin_title'] = 'AI Helper plugin';
|
||||
$strings['plugin_comment'] = 'Connects to AI services to help teachers and students through the generation of smart content, like creating questions on any given topic.';
|
||||
$strings['Description'] = 'Artificial Intelligence services (to use the broad term) now allow you to ask for meaningful texts to be generated, we can use those systems to pre-generate content, then let the teacher/trainer review the content before publication. See Aiken import page in the exercises tool once enabled.';
|
||||
$strings['tool_enable'] = 'Enable plugin';
|
||||
$strings['api_name'] = 'AI API to use';
|
||||
$strings['api_key'] = 'Api key';
|
||||
$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';
|
||||
$strings['tool_lp_enable'] = 'Enable in learning paths';
|
||||
$strings['tool_quiz_enable'] = 'Enable in exercises (Aiken section)';
|
||||
$strings['tokens_limit'] = 'AI tokens limit';
|
||||
$strings['tokens_limit_help'] = 'Limit the maximum number of tokens each user has available per month, to avoid high costs of service.';
|
||||
$strings['ErrorTokensLimit'] = 'Sorry, you have reached the maximum number of tokens or requests configured by the platform administrator for the current calendar month. Please contact your support team or wait for next month before you can issue new requests to the AI Helper.';
|
||||
18
plugin/ai_helper/lang/french.php
Normal file
18
plugin/ai_helper/lang/french.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
$strings['plugin_title'] = 'Assistant IA.';
|
||||
$strings['plugin_comment'] = "Système d'intelligence artificielle (IA) intégré aux endroits qui semblent les plus utiles aux enseignants pour la génération de contenu d'exemple, comme la création de banques de questions sur n'importe quel sujet.";
|
||||
$strings['Description'] = "L'intelligence artificielle (pour utiliser le terme large) disponible aujourd'hui vous permet de demander la génération de textes réalistes sur n'importe quel sujet. Vous pouvez utiliser ces systèmes pour pré-générer du contenu, et que l'enseignant/formateur puisse perfectionner le contenu avant sa publication.";
|
||||
$strings['tool_enable'] = 'Activer le plug-in';
|
||||
$strings['api_name'] = "API de l'IA pour se connecter";
|
||||
$strings['api_key'] = "API key";
|
||||
$strings['api_key_help'] = 'Clé secrète générée pour votre API AI';
|
||||
$strings['organization_id'] = "ID de l'organisation";
|
||||
$strings['organization_id_help'] = "Si votre compte api provient d'une organisation.";
|
||||
$strings['OpenAI'] = 'OpenAI';
|
||||
$strings['tool_lp_enable'] = "Activer dans les parcours";
|
||||
$strings['tool_quiz_enable'] = "Activer dans les exercices";
|
||||
$strings['tokens_limit'] = "Limite de jetons IA";
|
||||
$strings['tokens_limit_help'] = 'Limiter le nombre maximum de jetons disponibles pour chaque utilisateur, par mois, pour éviter des frais de service surdimensionnés.';
|
||||
$strings['ErrorTokensLimit'] = 'Désolé, vous avez atteint le nombre maximum de jetons ou de requêtes configurés par l\'administrateur de la plateforme pour le mois civil en cours. Veuillez contacter votre équipe d\'assistance ou attendre le mois prochain avant de pouvoir envoyer de nouvelles demandes à AI Helper.';
|
||||
21
plugin/ai_helper/lang/spanish.php
Normal file
21
plugin/ai_helper/lang/spanish.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
$strings['plugin_title'] = 'Asistente de IA.';
|
||||
$strings['plugin_comment'] = 'Este plugin se integra en la plataforma para habilitar la generación automatizada de contenido inteligente, como preguntas de elección múltiple.';
|
||||
$strings['Description'] = 'Debido a que la Inteligencia Artificial (para usar el término amplio) disponible ahora nos permite solicitar que se generen textos significativos, podemos usar esos sistemas para generar contenido previamente y luego dejar que el maestro/entrenador revise el contenido antes de publicarlo..';
|
||||
$strings['tool_enable'] = 'Habilitar complemento';
|
||||
$strings['api_name'] = 'API IA para conectar';
|
||||
$strings['api_key'] = 'Clave API';
|
||||
$strings['api_key_help'] = 'Clave secreta generada para su API de IA';
|
||||
$strings['organization_id'] = 'Identificación de la organización';
|
||||
$strings['organization_id_help'] = 'En caso de que su cuenta API sea de una organización.';
|
||||
$strings['OpenAI'] = 'OpenAI';
|
||||
$strings['tool_lp_enable'] = "Activarlo en las lecciones";
|
||||
$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.';
|
||||
6
plugin/ai_helper/plugin.php
Normal file
6
plugin/ai_helper/plugin.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
require_once __DIR__.'/AiHelperPlugin.php';
|
||||
|
||||
$plugin_info = AiHelperPlugin::create()->get_info();
|
||||
91
plugin/ai_helper/src/deepseek/DeepSeek.php
Normal file
91
plugin/ai_helper/src/deepseek/DeepSeek.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
require_once 'DeepSeekUrl.php';
|
||||
|
||||
class DeepSeek
|
||||
{
|
||||
private $apiKey;
|
||||
private $headers;
|
||||
|
||||
public function __construct(string $apiKey)
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
$this->headers = [
|
||||
'Content-Type: application/json',
|
||||
"Authorization: Bearer {$this->apiKey}",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate questions using the DeepSeek API.
|
||||
*
|
||||
* @param array $payload Data to send to the API
|
||||
*
|
||||
* @throws Exception If an error occurs during the request
|
||||
*
|
||||
* @return string Decoded response from the API
|
||||
*/
|
||||
public function generateQuestions(array $payload): string
|
||||
{
|
||||
$url = DeepSeekUrl::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
|
||||
*
|
||||
* @throws Exception If a cURL error occurs
|
||||
*
|
||||
* @return string Raw response from the API
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
17
plugin/ai_helper/src/deepseek/DeepSeekUrl.php
Normal file
17
plugin/ai_helper/src/deepseek/DeepSeekUrl.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
class DeepSeekUrl
|
||||
{
|
||||
private const BASE_URL = 'https://api.deepseek.com/chat';
|
||||
|
||||
/**
|
||||
* Get the endpoint URL for chat completions.
|
||||
*
|
||||
* @return string URL for the chat completions endpoint
|
||||
*/
|
||||
public static function completionsUrl(): string
|
||||
{
|
||||
return self::BASE_URL.'/completions';
|
||||
}
|
||||
}
|
||||
341
plugin/ai_helper/src/openai/OpenAi.php
Normal file
341
plugin/ai_helper/src/openai/OpenAi.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
require_once 'OpenAiUrl.php';
|
||||
|
||||
class OpenAi
|
||||
{
|
||||
private $model = "gpt-4o"; // See https://platform.openai.com/docs/models for possible models
|
||||
private $headers;
|
||||
private $contentTypes;
|
||||
private $timeout = 0;
|
||||
private $streamMethod;
|
||||
|
||||
public function __construct(
|
||||
string $apiKey,
|
||||
string $organizationId = ''
|
||||
) {
|
||||
$this->contentTypes = [
|
||||
"application/json" => "Content-Type: application/json",
|
||||
"multipart/form-data" => "Content-Type: multipart/form-data",
|
||||
];
|
||||
|
||||
$this->headers = [
|
||||
$this->contentTypes["application/json"],
|
||||
"Authorization: Bearer $apiKey",
|
||||
];
|
||||
|
||||
if (!empty($organizationId)) {
|
||||
$this->headers[] = "OpenAI-Organization: $organizationId";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|string
|
||||
*/
|
||||
public function listModels()
|
||||
{
|
||||
$url = OpenAiUrl::fineTuneModel();
|
||||
|
||||
return $this->sendRequest($url, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $model
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function retrieveModel($model)
|
||||
{
|
||||
$model = "/$model";
|
||||
$url = OpenAiUrl::fineTuneModel().$model;
|
||||
|
||||
return $this->sendRequest($url, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
* @param null $stream
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function completion(array $opts, callable $stream = null)
|
||||
{
|
||||
if ($stream !== null && isset($opts['stream']) && $opts['stream']) {
|
||||
$this->streamMethod = $stream;
|
||||
}
|
||||
|
||||
$opts['model'] = $opts['model'] ?? $this->model;
|
||||
$url = OpenAiUrl::completionsURL();
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function createEdit($opts)
|
||||
{
|
||||
$url = OpenAiUrl::editsUrl();
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function image($opts)
|
||||
{
|
||||
$url = OpenAiUrl::imageUrl()."/generations";
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function imageEdit($opts)
|
||||
{
|
||||
$url = OpenAiUrl::imageUrl()."/edits";
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function createImageVariation($opts)
|
||||
{
|
||||
$url = OpenAiUrl::imageUrl()."/variations";
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function moderation($opts)
|
||||
{
|
||||
$url = OpenAiUrl::moderationUrl();
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function uploadFile($opts)
|
||||
{
|
||||
$url = OpenAiUrl::filesUrl();
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|string
|
||||
*/
|
||||
public function listFiles()
|
||||
{
|
||||
$url = OpenAiUrl::filesUrl();
|
||||
|
||||
return $this->sendRequest($url, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fileId
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function retrieveFile($fileId)
|
||||
{
|
||||
$fileId = "/$fileId";
|
||||
$url = OpenAiUrl::filesUrl().$fileId;
|
||||
|
||||
return $this->sendRequest($url, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fileId
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function retrieveFileContent($fileId)
|
||||
{
|
||||
$fileId = "/$fileId/content";
|
||||
$url = OpenAiUrl::filesUrl().$fileId;
|
||||
|
||||
return $this->sendRequest($url, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fileId
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function deleteFile($fileId)
|
||||
{
|
||||
$fileId = "/$fileId";
|
||||
$url = OpenAiUrl::filesUrl().$fileId;
|
||||
|
||||
return $this->sendRequest($url, 'DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function createFineTune($opts)
|
||||
{
|
||||
$url = OpenAiUrl::fineTuneUrl();
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|string
|
||||
*/
|
||||
public function listFineTunes()
|
||||
{
|
||||
$url = OpenAiUrl::fineTuneUrl();
|
||||
|
||||
return $this->sendRequest($url, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fineTuneId
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function retrieveFineTune($fineTuneId)
|
||||
{
|
||||
$fineTuneId = "/$fineTuneId";
|
||||
$url = OpenAiUrl::fineTuneUrl().$fineTuneId;
|
||||
|
||||
return $this->sendRequest($url, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fineTuneId
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function cancelFineTune($fineTuneId)
|
||||
{
|
||||
$fineTuneId = "/$fineTuneId/cancel";
|
||||
$url = OpenAiUrl::fineTuneUrl().$fineTuneId;
|
||||
|
||||
return $this->sendRequest($url, 'POST');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fineTuneId
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function listFineTuneEvents($fineTuneId)
|
||||
{
|
||||
$fineTuneId = "/$fineTuneId/events";
|
||||
$url = OpenAiUrl::fineTuneUrl().$fineTuneId;
|
||||
|
||||
return $this->sendRequest($url, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fineTuneId
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function deleteFineTune($fineTuneId)
|
||||
{
|
||||
$fineTuneId = "/$fineTuneId";
|
||||
$url = OpenAiUrl::fineTuneModel().$fineTuneId;
|
||||
|
||||
return $this->sendRequest($url, 'DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $opts
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function embeddings($opts)
|
||||
{
|
||||
$url = OpenAiUrl::embeddings();
|
||||
|
||||
return $this->sendRequest($url, 'POST', $opts);
|
||||
}
|
||||
|
||||
public function setTimeout(int $timeout)
|
||||
{
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
private function sendRequest(string $url, string $method, array $opts = []): string
|
||||
{
|
||||
$post_fields = json_encode($opts);
|
||||
|
||||
if (isset($opts['file']) || isset($opts['image'])) {
|
||||
$this->headers[0] = $this->contentTypes["multipart/form-data"];
|
||||
$post_fields = $opts;
|
||||
} else {
|
||||
$this->headers[0] = $this->contentTypes["application/json"];
|
||||
}
|
||||
$curl_info = [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_POSTFIELDS => $post_fields,
|
||||
CURLOPT_HTTPHEADER => $this->headers,
|
||||
];
|
||||
|
||||
if (empty($opts)) {
|
||||
unset($curl_info[CURLOPT_POSTFIELDS]);
|
||||
}
|
||||
|
||||
if (isset($opts['stream']) && $opts['stream']) {
|
||||
$curl_info[CURLOPT_WRITEFUNCTION] = $this->streamMethod;
|
||||
}
|
||||
|
||||
$curl = curl_init();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
74
plugin/ai_helper/src/openai/OpenAiUrl.php
Normal file
74
plugin/ai_helper/src/openai/OpenAiUrl.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
class OpenAiUrl
|
||||
{
|
||||
public const ORIGIN = 'https://api.openai.com';
|
||||
public const API_VERSION = 'v1';
|
||||
public const OPEN_AI_URL = self::ORIGIN."/".self::API_VERSION;
|
||||
|
||||
public static function completionsURL(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/completions";
|
||||
}
|
||||
|
||||
public static function editsUrl(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/edits";
|
||||
}
|
||||
|
||||
public static function searchURL(string $engine): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/engines/$engine/search";
|
||||
}
|
||||
|
||||
public static function enginesUrl(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/engines";
|
||||
}
|
||||
|
||||
public static function engineUrl(string $engine): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/engines/$engine";
|
||||
}
|
||||
|
||||
public static function classificationsUrl(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/classifications";
|
||||
}
|
||||
|
||||
public static function moderationUrl(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/moderations";
|
||||
}
|
||||
|
||||
public static function filesUrl(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/files";
|
||||
}
|
||||
|
||||
public static function fineTuneUrl(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/fine-tunes";
|
||||
}
|
||||
|
||||
public static function fineTuneModel(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/models";
|
||||
}
|
||||
|
||||
public static function answersUrl(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/answers";
|
||||
}
|
||||
|
||||
public static function imageUrl(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/images";
|
||||
}
|
||||
|
||||
public static function embeddings(): string
|
||||
{
|
||||
return self::OPEN_AI_URL."/embeddings";
|
||||
}
|
||||
}
|
||||
30
plugin/ai_helper/tool/answers.php
Normal file
30
plugin/ai_helper/tool/answers.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
/**
|
||||
Answer questions based on existing knowledge.
|
||||
*/
|
||||
require_once __DIR__.'/../../../main/inc/global.inc.php';
|
||||
require_once __DIR__.'/../AiHelperPlugin.php';
|
||||
|
||||
$plugin = AiHelperPlugin::create();
|
||||
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
218
plugin/ai_helper/tool/learnpath.php
Normal file
218
plugin/ai_helper/tool/learnpath.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
/**
|
||||
Create a learnpath with contents based on existing knowledge.
|
||||
*/
|
||||
|
||||
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
|
||||
|
||||
require_once __DIR__.'/../../../main/inc/global.inc.php';
|
||||
require_once __DIR__.'/../AiHelperPlugin.php';
|
||||
require_once api_get_path(SYS_CODE_PATH).'exercise/export/aiken/aiken_classes.php';
|
||||
require_once api_get_path(SYS_CODE_PATH).'exercise/export/aiken/aiken_import.inc.php';
|
||||
|
||||
$plugin = AiHelperPlugin::create();
|
||||
|
||||
$apiList = $plugin->getApiList();
|
||||
$apiName = $plugin->get('api_name');
|
||||
|
||||
if (!in_array($apiName, array_keys($apiList))) {
|
||||
echo json_encode(['success' => false, 'text' => 'AI Provider not available.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$courseLanguage = (string) $_REQUEST['language'];
|
||||
$chaptersCount = (int) $_REQUEST['nro_items'];
|
||||
$topic = (string) $_REQUEST['lp_name'];
|
||||
$wordsCount = (int) $_REQUEST['words_count'];
|
||||
$courseCode = (string) $_REQUEST['course_code'];
|
||||
$sessionId = (int) $_REQUEST['session_id'];
|
||||
$addTests = ('true' === $_REQUEST['add_tests']);
|
||||
$nQ = ($addTests ? (int) $_REQUEST['nro_questions'] : 0);
|
||||
|
||||
$messageGetItems = 'Generate the table of contents of a course in "%s" in %d or fewer chapters on the topic "%s". Return it as a list of items separated by new lines. Do not include a conclusion chapter.';
|
||||
$prompt = sprintf($messageGetItems, $courseLanguage, $chaptersCount, $topic);
|
||||
|
||||
$resultText = $plugin->getCompletionText($prompt, 'learnpath');
|
||||
|
||||
if (isset($resultText['error']) && $resultText['error']) {
|
||||
echo json_encode(['success' => false, 'text' => $resultText['message']]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($resultText)) {
|
||||
echo json_encode(['success' => false, 'text' => 'AI returned no results.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$lpItems = [];
|
||||
if (!empty($resultText)) {
|
||||
$style = api_get_css_asset('bootstrap/dist/css/bootstrap.min.css');
|
||||
$style .= api_get_css_asset('fontawesome/css/font-awesome.min.css');
|
||||
$style .= api_get_css(ChamiloApi::getEditorDocStylePath());
|
||||
$style .= api_get_css_asset('ckeditor/plugins/codesnippet/lib/highlight/styles/default.css');
|
||||
$style .= api_get_asset('ckeditor/plugins/codesnippet/lib/highlight/highlight.pack.js');
|
||||
$style .= '<script>hljs.initHighlightingOnLoad();</script>';
|
||||
|
||||
$items = explode("\n", $resultText);
|
||||
$position = 1;
|
||||
foreach ($items as $item) {
|
||||
if (substr($item, 0, 2) === '- ') {
|
||||
$item = substr($item, 2);
|
||||
}
|
||||
$explodedItem = preg_split('/\d\./', $item);
|
||||
$title = count($explodedItem) > 1 ? $explodedItem[1] : $explodedItem[0];
|
||||
if (!empty($title)) {
|
||||
$lpItems[$position]['title'] = trim($title);
|
||||
$messageGetItemContent = 'In the context of "%s", generate a document with HTML tags in "%s" with %d words of content or less, about "%s", as to be included as one chapter in a larger document on "%s". Consider the context is established for the reader and you do not need to repeat it.';
|
||||
$promptItem = sprintf($messageGetItemContent, $topic, $courseLanguage, $wordsCount, $title, $topic);
|
||||
$resultContentText = $plugin->getCompletionText($promptItem, 'learnpath');
|
||||
if (isset($resultContentText['error']) && $resultContentText['error']) {
|
||||
continue;
|
||||
}
|
||||
$lpItemContent = (!empty($resultContentText) ? trim($resultContentText) : '');
|
||||
if (false !== stripos($lpItemContent, '</head>')) {
|
||||
$lpItemContent = preg_replace("|</head>|i", "\r\n$style\r\n\\0", $lpItemContent);
|
||||
} else {
|
||||
$lpItemContent = '<html><head><title>'.trim($title).'</title>'.$style.'</head><body>'.$lpItemContent.'</body></html>';
|
||||
}
|
||||
$lpItems[$position]['content'] = $lpItemContent;
|
||||
$position++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the learnpath and return the id generated.
|
||||
$return = ['success' => false, 'lp_id' => 0];
|
||||
if (!empty($lpItems)) {
|
||||
$lpId = learnpath::add_lp(
|
||||
$courseCode,
|
||||
$topic,
|
||||
'',
|
||||
'chamilo',
|
||||
'manual'
|
||||
);
|
||||
|
||||
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($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->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++;
|
||||
}
|
||||
|
||||
// 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);
|
||||
16
plugin/ai_helper/uninstall.php
Normal file
16
plugin/ai_helper/uninstall.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Uninstall the Ai Helper Plugin.
|
||||
*
|
||||
* @package chamilo.plugin.ai_helper
|
||||
*/
|
||||
require_once __DIR__.'/../../main/inc/global.inc.php';
|
||||
require_once __DIR__.'/AiHelperPlugin.php';
|
||||
|
||||
if (!api_is_platform_admin()) {
|
||||
exit('You must have admin permissions to install plugins');
|
||||
}
|
||||
|
||||
AiHelperPlugin::create()->uninstall();
|
||||
Reference in New Issue
Block a user