Passed
Pull Request — master (#7304)
by Yannick
10:52 queued 01:00
created

GrokProvider::generateQuestions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 17
c 0
b 0
f 0
nc 1
nop 4
dl 0
loc 21
rs 9.7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Chamilo\CoreBundle\AiProvider;
6
7
use Chamilo\CoreBundle\Entity\AiRequests;
8
use Chamilo\CoreBundle\Repository\AiRequestsRepository;
9
use Chamilo\CoreBundle\Settings\SettingsManager;
10
use Exception;
11
use RuntimeException;
12
use Symfony\Bundle\SecurityBundle\Security;
13
use Symfony\Component\Security\Core\User\UserInterface;
14
use Symfony\Contracts\HttpClient\HttpClientInterface;
15
16
class GrokProvider implements AiProviderInterface
17
{
18
    private string $apiUrl;
19
    private string $apiKey;
20
    private string $model;
21
    private float $temperature;
22
    private HttpClientInterface $httpClient;
23
    private AiRequestsRepository $aiRequestsRepository;
24
    private Security $security;
25
26
    public function __construct(
27
        HttpClientInterface $httpClient,
28
        SettingsManager $settingsManager,
29
        AiRequestsRepository $aiRequestsRepository,
30
        Security $security
31
    ) {
32
        $this->httpClient = $httpClient;
33
        $this->aiRequestsRepository = $aiRequestsRepository;
34
        $this->security = $security;
35
36
        // Get AI providers from settings
37
        $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);
38
        $config = json_decode($configJson, true) ?? [];
39
40
        if (!isset($config['grok'])) {
41
            throw new RuntimeException('Grok configuration is missing.');
42
        }
43
        if (!isset($config['grok']['text'])) {
44
            throw new RuntimeException('Grok configuration for text processing is missing.');
45
        }
46
47
        $this->apiKey = $config['grok']['api_key'] ?? '';
48
        $this->apiUrl = $config['grok']['text']['url'] ?? 'https://api.x.ai/v1/chat/completions';
49
        $this->model = $config['grok']['text']['model'] ?? 'grok-beta';
50
        $this->temperature = $config['grok']['text']['temperature'] ?? 0.7;
51
52
        if (empty($this->apiKey)) {
53
            throw new RuntimeException('Grok API key is missing.');
54
        }
55
    }
56
57
    public function generateQuestions(string $topic, int $numQuestions, string $questionType, string $language): ?string
58
    {
59
        $prompt = \sprintf(
60
            'Generate %d "%s" questions in Aiken format in the %s language about "%s".
61
            Ensure each question follows this format:
62
63
            1. The question text.
64
            A. Option A
65
            B. Option B
66
            C. Option C
67
            D. Option D
68
            ANSWER: (Correct answer letter)
69
70
            The output should be plain text without additional symbols or markdown.',
71
            $numQuestions,
72
            $questionType,
73
            $language,
74
            $topic
75
        );
76
77
        return $this->requestGrokAI($prompt, 'quiz');
78
    }
79
80
    public function generateLearnPath(string $topic, int $chaptersCount, string $language, int $wordsCount, bool $addTests, int $numQuestions): ?array
81
    {
82
        // Step 1: Generate the Table of Contents
83
        $tableOfContentsPrompt = \sprintf(
84
            'Generate a structured table of contents for a course in "%s" with %d chapters on "%s".
85
            Return a numbered list, each chapter on a new line. No conclusion.',
86
            $language,
87
            $chaptersCount,
88
            $topic
89
        );
90
91
        $lpStructure = $this->requestGrokAI($tableOfContentsPrompt, 'learnpath');
92
        if (!$lpStructure) {
93
            return ['success' => false, 'message' => 'Failed to generate course structure.'];
94
        }
95
96
        // Step 2: Generate content for each chapter
97
        $lpItems = [];
98
        $chapters = explode("\n", trim($lpStructure));
99
        foreach ($chapters as $index => $chapterTitle) {
100
            $chapterTitle = trim($chapterTitle);
101
            if (empty($chapterTitle)) {
102
                continue;
103
            }
104
105
            $chapterPrompt = \sprintf(
106
                'Create a learning chapter in HTML for "%s" in "%s" with %d words.
107
                Title: "%s". Assume the reader already knows the context.',
108
                $topic,
109
                $language,
110
                $wordsCount,
111
                $chapterTitle
112
            );
113
114
            $chapterContent = $this->requestGrokAI($chapterPrompt, 'learnpath');
115
            if (!$chapterContent) {
116
                continue;
117
            }
118
119
            $lpItems[] = [
120
                'title' => $chapterTitle,
121
                'content' => "<html><head><title>{$chapterTitle}</title></head><body>{$chapterContent}</body></html>",
122
            ];
123
        }
124
125
        // Step 3: Generate quizzes if enabled
126
        $quizItems = [];
127
        if ($addTests) {
128
            foreach ($lpItems as &$chapter) {
129
                $quizPrompt = \sprintf(
130
                    'Generate %d multiple-choice questions in Aiken format in %s about "%s".
131
            Ensure each question follows this format:
132
133
            1. The question text.
134
            A. Option A
135
            B. Option B
136
            C. Option C
137
            D. Option D
138
            ANSWER: (Correct answer letter)
139
140
            Each question must have exactly 4 options and one answer line.
141
            Return only valid questions without extra text.',
142
                    $numQuestions,
143
                    $language,
144
                    $chapter['title']
145
                );
146
147
                $quizContent = $this->requestGrokAI($quizPrompt, 'learnpath');
148
149
                if ($quizContent) {
150
                    $validQuestions = $this->filterValidAikenQuestions($quizContent);
151
152
                    if (!empty($validQuestions)) {
153
                        $quizItems[] = [
154
                            'title' => 'Quiz: '.$chapter['title'],
155
                            'content' => implode("\n\n", $validQuestions),
156
                        ];
157
                    }
158
                }
159
            }
160
        }
161
162
        return [
163
            'success' => true,
164
            'topic' => $topic,
165
            'lp_items' => $lpItems,
166
            'quiz_items' => $quizItems,
167
        ];
168
    }
169
170
    private function filterValidAikenQuestions(string $quizContent): array
171
    {
172
        $questions = preg_split('/\n{2,}/', trim($quizContent));
173
174
        $validQuestions = [];
175
        foreach ($questions as $questionBlock) {
176
            $lines = explode("\n", trim($questionBlock));
177
178
            if (\count($lines) < 6) {
179
                continue;
180
            }
181
182
            $options = \array_slice($lines, 1, 4);
183
            $validOptions = array_filter($options, fn ($line) => preg_match('/^[A-D]\. .+/', $line));
184
185
            $answerLine = end($lines);
186
            if (4 === \count($validOptions) && preg_match('/^ANSWER: [A-D]$/', $answerLine)) {
187
                $validQuestions[] = implode("\n", $lines);
188
            }
189
        }
190
191
        return $validQuestions;
192
    }
193
194
    private function requestGrokAI(string $prompt, string $toolName): ?string
195
    {
196
        $userId = $this->getUserId();
197
        if (!$userId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userId of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
198
            throw new RuntimeException('User not authenticated.');
199
        }
200
201
        $payload = [
202
            'model' => $this->model,
203
            'input' => [  // Changed from 'messages'
204
                ['role' => 'system', 'content' => 'You are a helpful AI assistant that generates structured educational content.'],
205
                ['role' => 'user', 'content' => $prompt],
206
            ],
207
            'temperature' => $this->temperature,
208
            'max_tokens' => 1000,
209
            // Optional: Add if needed, e.g., 'store' => true, 'parallel_tool_calls' => false
210
        ];
211
212
        try {
213
            $response = $this->httpClient->request('POST', $this->apiUrl, [
214
                'headers' => [
215
                    'Authorization' => 'Bearer '.$this->apiKey,
216
                    'Content-Type' => 'application/json',
217
                ],
218
                'json' => $payload,
219
            ]);
220
221
            $statusCode = $response->getStatusCode();
222
            $data = $response->toArray();
223
224
            if (200 === $statusCode && isset($data['output'][0]['content'][0]['text']) && 'completed' === $data['status']) {  // Updated parsing and status check
225
                $generatedContent = $data['output'][0]['content'][0]['text'];
226
227
                $aiRequest = new AiRequests();
228
                $aiRequest->setUserId($userId)
229
                    ->setToolName($toolName)
230
                    ->setRequestText($prompt)
231
                    ->setPromptTokens($data['usage']['prompt_tokens'] ?? 0)
232
                    ->setCompletionTokens($data['usage']['completion_tokens'] ?? 0)
233
                    ->setTotalTokens($data['usage']['total_tokens'] ?? 0)
234
                    ->setAiProvider('grok')
235
                ;
236
237
                $this->aiRequestsRepository->save($aiRequest);
238
239
                return $generatedContent;
240
            }
241
242
            return null;
243
        } catch (Exception $e) {
244
            error_log('[AI][Grok] Exception: '.$e->getMessage());
245
246
            return null;
247
        }
248
    }
249
250
    public function gradeOpenAnswer(string $prompt, string $toolName): ?string
251
    {
252
        return $this->requestGrokAI($prompt, $toolName);
253
    }
254
255
    private function getUserId(): ?int
256
    {
257
        $user = $this->security->getUser();
258
259
        return $user instanceof UserInterface ? $user->getId() : null;
260
    }
261
}
262