Passed
Pull Request — master (#6112)
by
unknown
09:39 queued 01:20
created

DeepSeekAiProvider::requestDeepSeekAI()   A

Complexity

Conditions 5
Paths 11

Size

Total Lines 51
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 34
c 0
b 0
f 0
nc 11
nop 2
dl 0
loc 51
rs 9.0648

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Service\AI;
8
9
use Chamilo\CoreBundle\Entity\AiRequests;
10
use Chamilo\CoreBundle\Repository\AiRequestsRepository;
11
use Chamilo\CoreBundle\Settings\SettingsManager;
12
use Symfony\Bundle\SecurityBundle\Security;
13
use Symfony\Component\Security\Core\User\UserInterface;
14
use Symfony\Contracts\HttpClient\HttpClientInterface;
15
16
class DeepSeekAiProvider implements AiProviderInterface
17
{
18
    private string $apiUrl;
19
    private string $apiKey;
20
    private string $model;
21
    private float $temperature;
22
    private string $organizationId;
23
    private int $monthlyTokenLimit;
24
    private HttpClientInterface $httpClient;
25
    private AiRequestsRepository $aiRequestsRepository;
26
    private Security $security;
27
28
    public function __construct(
29
        HttpClientInterface $httpClient,
30
        SettingsManager $settingsManager,
31
        AiRequestsRepository $aiRequestsRepository,
32
        Security $security
33
    ) {
34
        $this->httpClient = $httpClient;
35
        $this->aiRequestsRepository = $aiRequestsRepository;
36
        $this->security = $security;
37
38
        // Get AI providers from settings
39
        $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);
40
        $config = json_decode($configJson, true) ?? [];
41
42
        if (!isset($config['deepseek'])) {
43
            throw new \RuntimeException('DeepSeek configuration is missing.');
44
        }
45
46
        $this->apiUrl = $config['deepseek']['url'] ?? 'https://api.deepseek.com/chat/completions';
47
        $this->apiKey = $config['deepseek']['api_key'] ?? '';
48
        $this->model = $config['deepseek']['model'] ?? 'deepseek-chat';
49
        $this->temperature = $config['deepseek']['temperature'] ?? 0.7;
50
        $this->organizationId = $config['deepseek']['organization_id'] ?? '';
51
        $this->monthlyTokenLimit = $config['deepseek']['monthly_token_limit'] ?? 1000;
52
53
        if (empty($this->apiKey)) {
54
            throw new \RuntimeException('DeepSeek API key is missing.');
55
        }
56
    }
57
58
    public function generateQuestions(string $topic, int $numQuestions, string $questionType, string $language): ?string
59
    {
60
        $prompt = sprintf(
61
            'Generate %d "%s" questions in Aiken format in the %s language about "%s".
62
            Ensure each question follows this format:
63
64
            1. The question text.
65
            A. Option A
66
            B. Option B
67
            C. Option C
68
            D. Option D
69
            ANSWER: (Correct answer letter)
70
71
            The output should be plain text without additional symbols or markdown.',
72
            $numQuestions, $questionType, $language, $topic
73
        );
74
75
        return $this->requestDeepSeekAI($prompt, 'quiz');
76
    }
77
78
    public function generateLearnPath(string $topic, int $chaptersCount, string $language, int $wordsCount, bool $addTests, int $numQuestions): ?array
79
    {
80
        // Step 1: Generate the Table of Contents
81
        $tableOfContentsPrompt = sprintf(
82
            'Generate a structured table of contents for a course in "%s" with %d chapters on "%s".
83
            Return a numbered list, each chapter on a new line. No conclusion.',
84
            $language, $chaptersCount, $topic
85
        );
86
87
        $lpStructure = $this->requestDeepSeekAI($tableOfContentsPrompt, 'learnpath');
88
        if (!$lpStructure) {
89
            return ['success' => false, 'message' => 'Failed to generate course structure.'];
90
        }
91
92
        // Step 2: Generate content for each chapter
93
        $lpItems = [];
94
        $chapters = explode("\n", trim($lpStructure));
95
        foreach ($chapters as $index => $chapterTitle) {
96
            $chapterTitle = trim($chapterTitle);
97
            if (empty($chapterTitle)) continue;
98
99
            $chapterPrompt = sprintf(
100
                'Create a learning chapter in HTML for "%s" in "%s" with %d words.
101
                Title: "%s". Assume the reader already knows the context.',
102
                $topic, $language, $wordsCount, $chapterTitle
103
            );
104
105
            $chapterContent = $this->requestDeepSeekAI($chapterPrompt, 'learnpath');
106
            if (!$chapterContent) continue;
107
108
            $lpItems[] = [
109
                'title' => $chapterTitle,
110
                'content' => "<html><head><title>{$chapterTitle}</title></head><body>{$chapterContent}</body></html>"
111
            ];
112
        }
113
114
        // Step 3: Generate quizzes if enabled
115
        $quizItems = [];
116
        if ($addTests) {
117
            foreach ($lpItems as &$chapter) {
118
                $quizPrompt = sprintf(
119
                    'Generate %d multiple-choice questions in Aiken format in %s about "%s".
120
        Ensure each question follows this format:
121
122
        1. The question text.
123
        A. Option A
124
        B. Option B
125
        C. Option C
126
        D. Option D
127
        ANSWER: (Correct answer letter)
128
129
        Each question must have exactly 4 options and one answer line.
130
        Return only valid questions without extra text.',
131
                    $numQuestions, $language, $chapter['title']
132
                );
133
134
                $quizContent = $this->requestDeepSeekAI($quizPrompt, 'learnpath');
135
136
                if ($quizContent) {
137
                    $validQuestions = $this->filterValidAikenQuestions($quizContent);
138
139
                    if (!empty($validQuestions)) {
140
                        $quizItems[] = [
141
                            'title' => "Quiz: " . $chapter['title'],
142
                            'content' => implode("\n\n", $validQuestions)
143
                        ];
144
                    }
145
                }
146
            }
147
        }
148
149
        return [
150
            'success' => true,
151
            'topic' => $topic,
152
            'lp_items' => $lpItems,
153
            'quiz_items' => $quizItems
154
        ];
155
    }
156
157
    private function filterValidAikenQuestions(string $quizContent): array
158
    {
159
        $questions = preg_split('/\n{2,}/', trim($quizContent));
160
161
        $validQuestions = [];
162
        foreach ($questions as $questionBlock) {
163
            $lines = explode("\n", trim($questionBlock));
164
165
            if (count($lines) < 6) {
166
                continue;
167
            }
168
169
            $options = array_slice($lines, 1, 4);
170
            $validOptions = array_filter($options, fn($line) => preg_match('/^[A-D]\. .+/', $line));
171
172
            $answerLine = end($lines);
173
            if (count($validOptions) === 4 && preg_match('/^ANSWER: [A-D]$/', $answerLine)) {
174
                $validQuestions[] = implode("\n", $lines);
175
            }
176
        }
177
178
        return $validQuestions;
179
    }
180
181
    private function requestDeepSeekAI(string $prompt, string $toolName): ?string
182
    {
183
        $userId = $this->getUserId();
184
        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...
185
            throw new \RuntimeException('User not authenticated.');
186
        }
187
188
        $payload = [
189
            'model' => $this->model,
190
            'messages' => [
191
                ['role' => 'system', 'content' => 'You are a helpful AI assistant that generates structured educational content.'],
192
                ['role' => 'user', 'content' => $prompt]
193
            ],
194
            'temperature' => $this->temperature,
195
            'max_tokens' => 300,
196
        ];
197
198
        try {
199
            $response = $this->httpClient->request('POST', $this->apiUrl, [
200
                'headers' => [
201
                    'Authorization' => 'Bearer ' . $this->apiKey,
202
                    'Content-Type' => 'application/json',
203
                ],
204
                'json' => $payload,
205
            ]);
206
207
            $statusCode = $response->getStatusCode();
208
            $data = $response->toArray();
209
210
            if ($statusCode === 200 && isset($data['choices'][0]['message']['content'])) {
211
                $generatedContent = $data['choices'][0]['message']['content'];
212
213
                $aiRequest = new AiRequests();
214
                $aiRequest->setUserId($userId)
215
                    ->setToolName($toolName)
216
                    ->setRequestText($prompt)
217
                    ->setPromptTokens($data['usage']['prompt_tokens'] ?? 0)
218
                    ->setCompletionTokens($data['usage']['completion_tokens'] ?? 0)
219
                    ->setTotalTokens($data['usage']['total_tokens'] ?? 0)
220
                    ->setAiProvider('deepseek');
221
222
                $this->aiRequestsRepository->save($aiRequest);
223
224
                return $generatedContent;
225
            }
226
227
            return null;
228
229
        } catch (\Exception $e) {
230
            error_log("ERROR - DeepSeek Request failed: " . $e->getMessage());
231
            return null;
232
        }
233
    }
234
235
    private function getUserId(): ?int
236
    {
237
        $user = $this->security->getUser();
238
        return $user instanceof UserInterface ? $user->getId() : null;
239
    }
240
}
241