Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chamilo_chamilo-lms.json
T

1 line
81 KiB
JSON

{"repo": "chamilo/chamilo-lms", "files": {"src/CoreBundle/AiProvider/DeepSeekProvider.php": {"71de18e7f34a8a3763bb66b083f595830b0ce0dd": {"t": 1785746143.9931307, "n": 17115, "h": "22a9eeb2bc3961dc", "c": "<?php\n\ndeclare(strict_types=1);\n\n/* For licensing terms, see /license.txt */\n\nnamespace Chamilo\\CoreBundle\\AiProvider;\n\nuse Chamilo\\CoreBundle\\Entity\\AiRequests;\nuse Chamilo\\CoreBundle\\Repository\\AiRequestsRepository;\nuse Chamilo\\CoreBundle\\Settings\\SettingsManager;\nuse Exception;\nuse RuntimeException;\nuse Symfony\\Bundle\\SecurityBundle\\Security;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass DeepSeekProvider implements AiProviderInterface, AiDocumentProviderInterface\n{\n private array $providerConfig;\n private string $apiUrl;\n private string $apiKey;\n private ?string $lastTextError = null;\n private string $model;\n private float $temperature;\n private int $maxTokens;\n\n public function __construct(\n private readonly HttpClientInterface $httpClient,\n SettingsManager $settingsManager,\n private readonly AiRequestsRepository $aiRequestsRepository,\n private readonly Security $security\n ) {\n $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);\n $config = \\is_string($configJson) ? (json_decode($configJson, true) ?? []) : (\\is_array($configJson) ? $configJson : []);\n\n if (!isset($config['deepseek']) || !\\is_array($config['deepseek'])) {\n throw new RuntimeException('DeepSeek configuration is missing.');\n }\n\n $this->providerConfig = $config['deepseek'];\n $this->apiKey = (string) ($this->providerConfig['api_key'] ?? '');\n\n if ('' === trim($this->apiKey)) {\n throw new RuntimeException('DeepSeek API key is missing.');\n }\n\n $textCfg = $this->providerConfig['text'] ?? [];\n if (!\\is_array($textCfg)) {\n $textCfg = [];\n }\n\n $this->apiUrl = (string) ($textCfg['url'] ?? 'https://api.deepseek.com/chat/completions');\n $this->model = (string) ($textCfg['model'] ?? 'deepseek-chat');\n $this->temperature = (float) ($textCfg['temperature'] ?? 0.7);\n $this->maxTokens = (int) ($textCfg['max_tokens'] ?? 1000);\n }\n\n /**\n * Chat-style entrypoint (OpenAI-compatible).\n *\n * @param array<int, array{role:string,content:string}> $messages\n * @param array<string,mixed> $options\n */\n public function chat(array $messages, array $options = []): string\n {\n $userId = $this->getUserId();\n if (!$userId) {\n error_log('[AI][DeepSeek][chat] User not authenticated.');\n\n return 'Error: User is not authenticated.';\n }\n\n $normalized = $this->normalizeChatMessages($messages);\n if (empty($normalized)) {\n error_log('[AI][DeepSeek][chat] Empty messages payload.');\n\n return 'Error: Empty chat messages.';\n }\n\n $resolved = $this->resolveTextOptions($options);\n\n $payload = [\n 'model' => $resolved['model'],\n 'messages' => $normalized,\n 'temperature' => $resolved['temperature'],\n 'max_tokens' => $resolved['max_tokens'],\n ];\n\n // Optional safe overrides\n if (isset($options['top_p'])) {\n $payload['top_p'] = (float) $options['top_p'];\n }\n if (isset($options['presence_penalty'])) {\n $payload['presence_penalty'] = (float) $options['presence_penalty'];\n }\n if (isset($options['frequency_penalty'])) {\n $payload['frequency_penalty'] = (float) $options['frequency_penalty'];\n }\n\n try {\n $response = $this->httpClient->request('POST', $resolved['url'], [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $data = $response->toArray(false);\n\n if (\\is_array($data) && isset($data['error'])) {\n $msg = $data['error']['message'] ?? 'DeepSeek returned an error response.';\n $msg = \\is_string($msg) ? trim($msg) : 'DeepSeek returned an error response.';\n error_log('[AI][DeepSeek][chat] Error response: '.$msg);\n\n return 'Error: '.$msg;\n }\n\n $generated = $data['choices'][0]['message']['content'] ?? null;\n if (!\\is_string($generated) || '' === trim($generated)) {\n error_log('[AI][DeepSeek][chat] Empty content returned.');\n\n return 'Error: Empty response from DeepSeek.';\n }\n\n $this->saveAiRequest(\n $userId,\n 'chat',\n $this->messagesForLog($normalized, 900),\n 'deepseek',\n (int) ($data['usage']['prompt_tokens'] ?? 0),\n (int) ($data['usage']['completion_tokens'] ?? 0),\n (int) ($data['usage']['total_tokens'] ?? 0)\n );\n\n return trim($generated);\n } catch (Exception $e) {\n error_log('[AI][DeepSeek][chat] Exception: '.$e->getMessage());\n\n return 'Error: '.$e->getMessage();\n }\n }\n\n /**\n * Prompt-style entrypoint used by TaskGrader text mode and other features.\n *\n * @param array<string,mixed> $options\n */\n public function generateText(string $prompt, array $options = []): string\n {\n $prompt = trim($prompt);\n if ('' === $prompt) {\n return 'Error: Empty prompt.';\n }\n\n $messages = [\n ['role' => 'system', 'content' => 'You are a helpful assistant.'],\n ['role' => 'user', 'content' => $prompt],\n ];\n\n return $this->chat($messages, $options);\n }\n\n public function generateQuestions(string $topic, int $numQuestions, string $questionType, string $language): ?string\n {\n $prompt = \\sprintf(\n 'Generate %d \"%s\" questions in Aiken format in the %s language about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n The output should be plain text without additional symbols or markdown.',\n $numQuestions,\n $questionType,\n $language,\n $topic\n );\n\n return $this->requestDeepSeek($prompt, 'quiz');\n }\n\n public function generateLearnPath(string $topic, int $chaptersCount, string $language, int $wordsCount, bool $addTests, int $numQuestions): ?array\n {\n $tableOfContentsPrompt = \\sprintf(\n 'Generate a structured table of contents for a course in \"%s\" with %d chapters on \"%s\".\n Return a numbered list, each chapter on a new line. No conclusion.',\n $language,\n $chaptersCount,\n $topic\n );\n\n $this->lastTextError = null;\n $lpStructure = $this->requestDeepSeek($tableOfContentsPrompt, 'learnpath');\n if (!$lpStructure) {\n return ['success' => false, 'message' => $this->buildTextFailureMessage('DeepSeek')];\n }\n\n $lpItems = [];\n $chapters = explode(\"\\n\", trim($lpStructure));\n foreach ($chapters as $chapterTitle) {\n $chapterTitle = trim($chapterTitle);\n if ('' === $chapterTitle) {\n continue;\n }\n\n $chapterPrompt = \\sprintf(\n 'Create a learning chapter in HTML for \"%s\" in \"%s\" with %d words.\n Title: \"%s\". Assume the reader already knows the context.',\n $topic,\n $language,\n $wordsCount,\n $chapterTitle\n );\n\n $chapterContent = $this->requestDeepSeek($chapterPrompt, 'learnpath');\n if (!$chapterContent) {\n continue;\n }\n\n $lpItems[] = [\n 'title' => $chapterTitle,\n 'content' => \"<html><head><title>{$chapterTitle}</title></head><body>{$chapterContent}</body></html>\",\n ];\n }\n\n $quizItems = [];\n if ($addTests) {\n foreach ($lpItems as $chapter) {\n $quizPrompt = \\sprintf(\n 'Generate %d multiple-choice questions in Aiken format in %s about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n Each question must have exactly 4 options and one answer line.\n Return only valid questions without extra text.',\n $numQuestions,\n $language,\n $chapter['title']\n );\n\n $quizContent = $this->requestDeepSeek($quizPrompt, 'learnpath');\n if ($quizContent) {\n $validQuestions = $this->filterValidAikenQuestions($quizContent);\n if (!empty($validQuestions)) {\n $quizItems[] = [\n 'title' => 'Quiz: '.$chapter['title'],\n 'content' => implode(\"\\n\\n\", $validQuestions),\n ];\n }\n }\n }\n }\n\n return [\n 'success' => true,\n 'topic' => $topic,\n 'lp_items' => $lpItems,\n 'quiz_items' => $quizItems,\n ];\n }\n\n public function gradeOpenAnswer(string $prompt, string $toolName): ?string\n {\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n public function generateDocument(string $prompt, string $toolName, ?array $options = []): ?string\n {\n // Document is text-like for DeepSeek\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n private function requestDeepSeek(string $prompt, string $toolName): ?string\n {\n $this->lastTextError = null;\n\n $userId = $this->getUserId();\n if (!$userId) {\n $this->lastTextError = 'User is not authenticated.';\n\n return null;\n }\n\n $payload = [\n 'model' => $this->model,\n 'messages' => [\n ['role' => 'system', 'content' => 'You are a helpful AI assistant that generates structured educational content.'],\n ['role' => 'user', 'content' => $prompt],\n ],\n 'temperature' => $this->temperature,\n 'max_tokens' => $this->maxTokens,\n ];\n\n try {\n $response = $this->httpClient->request('POST', $this->apiUrl, [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $statusCode = $response->getStatusCode();\n $data = $response->toArray(false);\n\n if (\\is_array($data) && isset($data['error'])) {\n $msg = $data['error']['message'] ?? 'DeepSeek returned an error response.';\n $msg = \\is_string($msg) ? trim($msg) : 'DeepSeek returned an error response.';\n $this->lastTextError = 'HTTP '.$statusCode.': '.$msg;\n error_log('[AI][DeepSeek] Error response: '.$msg);\n\n return null;\n }\n\n $generatedContent = $data['choices'][0]['message']['content'] ?? null;\n if (!\\is_string($generatedContent) || '' === trim($generatedContent)) {\n $this->lastTextError = 'HTTP '.$statusCode.': Empty content returned.';\n error_log('[AI][DeepSeek] Empty content returned.');\n\n return null;\n }\n\n $this->saveAiRequest(\n $userId,\n $toolName,\n $prompt,\n 'deepseek',\n (int) ($data['usage']['prompt_tokens'] ?? 0),\n (int) ($data['usage']['completion_tokens'] ?? 0),\n (int) ($data['usage']['total_tokens'] ?? 0)\n );\n\n return $generatedContent;\n } catch (Exception $e) {\n $this->lastTextError = $e->getMessage();\n error_log('[AI][DeepSeek] Exception: '.$e->getMessage());\n\n return null;\n }\n }\n\n private function buildTextFailureMessage(string $provider): string\n {\n $detail = trim((string) $this->lastTextError);\n if ('' === $detail) {\n return $provider.' failed to generate the course structure.';\n }\n\n return $provider.' failed to generate the course structure: '.mb_substr($detail, 0, 1000);\n }\n\n private function filterValidAikenQuestions(string $quizContent): array\n {\n $questions = preg_split('/\\n{2,}/', trim($quizContent)) ?: [];\n\n $validQuestions = [];\n foreach ($questions as $questionBlock) {\n $lines = explode(\"\\n\", trim($questionBlock));\n\n if (\\count($lines) < 6) {\n continue;\n }\n\n $options = \\array_slice($lines, 1, 4);\n $validOptions = array_filter($options, static fn ($line) => (bool) preg_match('/^[A-D]\\. .+/', $line));\n\n $answerLine = (string) end($lines);\n if (4 === \\count($validOptions) && preg_match('/^ANSWER: [A-D]$/', $answerLine)) {\n $validQuestions[] = implode(\"\\n\", $lines);\n }\n }\n\n return $validQuestions;\n }\n\n /**\n * @param array<int, array{role:string,content:string}> $messages\n *\n * @return array<int, array{role:string,content:string}>\n */\n private function normalizeChatMessages(array $messages): array\n {\n $out = [];\n\n foreach ($messages as $m) {\n if (!\\is_array($m)) {\n continue;\n }\n\n $role = isset($m['role']) ? trim((string) $m['role']) : '';\n $content = isset($m['content']) ? trim((string) $m['content']) : '';\n\n if ('' === $role || '' === $content) {\n continue;\n }\n\n $role = strtolower($role);\n if (!\\in_array($role, ['system', 'user', 'assistant', 'tool'], true)) {\n $role = 'user';\n }\n\n $out[] = [\n 'role' => $role,\n 'content' => $content,\n ];\n }\n\n return $out;\n }\n\n /**\n * @param array<int, array{role:string,content:string}> $messages\n */\n private function messagesForLog(array $messages, int $maxChars = 900): string\n {\n $parts = [];\n foreach ($messages as $m) {\n $role = $m['role'] ?? 'user';\n $content = trim((string) ($m['content'] ?? ''));\n\n if ('' === $content) {\n continue;\n }\n\n $parts[] = strtoupper((string) $role).': '.mb_substr($content, 0, 300);\n }\n\n $s = implode(' | ', $parts);\n\n return mb_substr($s, 0, $maxChars);\n }\n\n /**\n * Resolve per-request overrides for text/chat.\n *\n * @param array<string,mixed> $options\n *\n * @return array{url:string,model:string,temperature:float,max_tokens:int}\n */\n private function resolveTextOptions(array $options): array\n {\n $url = (string) (($options['url'] ?? null) ?? $this->apiUrl);\n $model = (string) (($options['model'] ?? null) ?? $this->model);\n\n $temperature = (float) (($options['temperature'] ?? null) ?? $this->temperature);\n\n // Accept either max_tokens or max_output_tokens (normalize to max_tokens).\n $maxTokens = $options['max_tokens'] ?? ($options['max_output_tokens'] ?? null);\n $maxTokens = (int) (($maxTokens ?? null) ?? $this->maxTokens);\n\n if ($maxTokens <= 0) {\n $maxTokens = $this->maxTokens > 0 ? $this->maxTokens : 1000;\n }\n\n return [\n 'url' => $url,\n 'model' => $model,\n 'temperature' => $temperature,\n 'max_tokens' => $maxTokens,\n ];\n }\n\n private function saveAiRequest(\n int $userId,\n string $toolName,\n string $requestText,\n string $provider,\n int $promptTokens = 0,\n int $completionTokens = 0,\n int $totalTokens = 0\n ): void {\n try {\n $aiRequest = new AiRequests();\n $aiRequest\n ->setUserId($userId)\n ->setToolName($toolName)\n ->setRequestText($requestText)\n ->setPromptTokens($promptTokens)\n ->setCompletionTokens($completionTokens)\n ->setTotalTokens($totalTokens)\n ->setAiProvider($provider)\n ;\n\n $this->aiRequestsRepository->save($aiRequest);\n } catch (Exception $e) {\n error_log('[AI] Failed to save AiRequests record: '.$e->getMessage());\n }\n }\n\n private function getUserId(): ?int\n {\n $user = $this->security->getUser();\n\n return $user instanceof UserInterface ? $user->getId() : null;\n }\n}\n"}, "dcc5b01812f5a42d22c38b782f7d876d4965ee3d": {"t": 1785760144.2972522, "n": 17115, "h": "22a9eeb2bc3961dc", "c": "<?php\n\ndeclare(strict_types=1);\n\n/* For licensing terms, see /license.txt */\n\nnamespace Chamilo\\CoreBundle\\AiProvider;\n\nuse Chamilo\\CoreBundle\\Entity\\AiRequests;\nuse Chamilo\\CoreBundle\\Repository\\AiRequestsRepository;\nuse Chamilo\\CoreBundle\\Settings\\SettingsManager;\nuse Exception;\nuse RuntimeException;\nuse Symfony\\Bundle\\SecurityBundle\\Security;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass DeepSeekProvider implements AiProviderInterface, AiDocumentProviderInterface\n{\n private array $providerConfig;\n private string $apiUrl;\n private string $apiKey;\n private ?string $lastTextError = null;\n private string $model;\n private float $temperature;\n private int $maxTokens;\n\n public function __construct(\n private readonly HttpClientInterface $httpClient,\n SettingsManager $settingsManager,\n private readonly AiRequestsRepository $aiRequestsRepository,\n private readonly Security $security\n ) {\n $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);\n $config = \\is_string($configJson) ? (json_decode($configJson, true) ?? []) : (\\is_array($configJson) ? $configJson : []);\n\n if (!isset($config['deepseek']) || !\\is_array($config['deepseek'])) {\n throw new RuntimeException('DeepSeek configuration is missing.');\n }\n\n $this->providerConfig = $config['deepseek'];\n $this->apiKey = (string) ($this->providerConfig['api_key'] ?? '');\n\n if ('' === trim($this->apiKey)) {\n throw new RuntimeException('DeepSeek API key is missing.');\n }\n\n $textCfg = $this->providerConfig['text'] ?? [];\n if (!\\is_array($textCfg)) {\n $textCfg = [];\n }\n\n $this->apiUrl = (string) ($textCfg['url'] ?? 'https://api.deepseek.com/chat/completions');\n $this->model = (string) ($textCfg['model'] ?? 'deepseek-chat');\n $this->temperature = (float) ($textCfg['temperature'] ?? 0.7);\n $this->maxTokens = (int) ($textCfg['max_tokens'] ?? 1000);\n }\n\n /**\n * Chat-style entrypoint (OpenAI-compatible).\n *\n * @param array<int, array{role:string,content:string}> $messages\n * @param array<string,mixed> $options\n */\n public function chat(array $messages, array $options = []): string\n {\n $userId = $this->getUserId();\n if (!$userId) {\n error_log('[AI][DeepSeek][chat] User not authenticated.');\n\n return 'Error: User is not authenticated.';\n }\n\n $normalized = $this->normalizeChatMessages($messages);\n if (empty($normalized)) {\n error_log('[AI][DeepSeek][chat] Empty messages payload.');\n\n return 'Error: Empty chat messages.';\n }\n\n $resolved = $this->resolveTextOptions($options);\n\n $payload = [\n 'model' => $resolved['model'],\n 'messages' => $normalized,\n 'temperature' => $resolved['temperature'],\n 'max_tokens' => $resolved['max_tokens'],\n ];\n\n // Optional safe overrides\n if (isset($options['top_p'])) {\n $payload['top_p'] = (float) $options['top_p'];\n }\n if (isset($options['presence_penalty'])) {\n $payload['presence_penalty'] = (float) $options['presence_penalty'];\n }\n if (isset($options['frequency_penalty'])) {\n $payload['frequency_penalty'] = (float) $options['frequency_penalty'];\n }\n\n try {\n $response = $this->httpClient->request('POST', $resolved['url'], [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $data = $response->toArray(false);\n\n if (\\is_array($data) && isset($data['error'])) {\n $msg = $data['error']['message'] ?? 'DeepSeek returned an error response.';\n $msg = \\is_string($msg) ? trim($msg) : 'DeepSeek returned an error response.';\n error_log('[AI][DeepSeek][chat] Error response: '.$msg);\n\n return 'Error: '.$msg;\n }\n\n $generated = $data['choices'][0]['message']['content'] ?? null;\n if (!\\is_string($generated) || '' === trim($generated)) {\n error_log('[AI][DeepSeek][chat] Empty content returned.');\n\n return 'Error: Empty response from DeepSeek.';\n }\n\n $this->saveAiRequest(\n $userId,\n 'chat',\n $this->messagesForLog($normalized, 900),\n 'deepseek',\n (int) ($data['usage']['prompt_tokens'] ?? 0),\n (int) ($data['usage']['completion_tokens'] ?? 0),\n (int) ($data['usage']['total_tokens'] ?? 0)\n );\n\n return trim($generated);\n } catch (Exception $e) {\n error_log('[AI][DeepSeek][chat] Exception: '.$e->getMessage());\n\n return 'Error: '.$e->getMessage();\n }\n }\n\n /**\n * Prompt-style entrypoint used by TaskGrader text mode and other features.\n *\n * @param array<string,mixed> $options\n */\n public function generateText(string $prompt, array $options = []): string\n {\n $prompt = trim($prompt);\n if ('' === $prompt) {\n return 'Error: Empty prompt.';\n }\n\n $messages = [\n ['role' => 'system', 'content' => 'You are a helpful assistant.'],\n ['role' => 'user', 'content' => $prompt],\n ];\n\n return $this->chat($messages, $options);\n }\n\n public function generateQuestions(string $topic, int $numQuestions, string $questionType, string $language): ?string\n {\n $prompt = \\sprintf(\n 'Generate %d \"%s\" questions in Aiken format in the %s language about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n The output should be plain text without additional symbols or markdown.',\n $numQuestions,\n $questionType,\n $language,\n $topic\n );\n\n return $this->requestDeepSeek($prompt, 'quiz');\n }\n\n public function generateLearnPath(string $topic, int $chaptersCount, string $language, int $wordsCount, bool $addTests, int $numQuestions): ?array\n {\n $tableOfContentsPrompt = \\sprintf(\n 'Generate a structured table of contents for a course in \"%s\" with %d chapters on \"%s\".\n Return a numbered list, each chapter on a new line. No conclusion.',\n $language,\n $chaptersCount,\n $topic\n );\n\n $this->lastTextError = null;\n $lpStructure = $this->requestDeepSeek($tableOfContentsPrompt, 'learnpath');\n if (!$lpStructure) {\n return ['success' => false, 'message' => $this->buildTextFailureMessage('DeepSeek')];\n }\n\n $lpItems = [];\n $chapters = explode(\"\\n\", trim($lpStructure));\n foreach ($chapters as $chapterTitle) {\n $chapterTitle = trim($chapterTitle);\n if ('' === $chapterTitle) {\n continue;\n }\n\n $chapterPrompt = \\sprintf(\n 'Create a learning chapter in HTML for \"%s\" in \"%s\" with %d words.\n Title: \"%s\". Assume the reader already knows the context.',\n $topic,\n $language,\n $wordsCount,\n $chapterTitle\n );\n\n $chapterContent = $this->requestDeepSeek($chapterPrompt, 'learnpath');\n if (!$chapterContent) {\n continue;\n }\n\n $lpItems[] = [\n 'title' => $chapterTitle,\n 'content' => \"<html><head><title>{$chapterTitle}</title></head><body>{$chapterContent}</body></html>\",\n ];\n }\n\n $quizItems = [];\n if ($addTests) {\n foreach ($lpItems as $chapter) {\n $quizPrompt = \\sprintf(\n 'Generate %d multiple-choice questions in Aiken format in %s about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n Each question must have exactly 4 options and one answer line.\n Return only valid questions without extra text.',\n $numQuestions,\n $language,\n $chapter['title']\n );\n\n $quizContent = $this->requestDeepSeek($quizPrompt, 'learnpath');\n if ($quizContent) {\n $validQuestions = $this->filterValidAikenQuestions($quizContent);\n if (!empty($validQuestions)) {\n $quizItems[] = [\n 'title' => 'Quiz: '.$chapter['title'],\n 'content' => implode(\"\\n\\n\", $validQuestions),\n ];\n }\n }\n }\n }\n\n return [\n 'success' => true,\n 'topic' => $topic,\n 'lp_items' => $lpItems,\n 'quiz_items' => $quizItems,\n ];\n }\n\n public function gradeOpenAnswer(string $prompt, string $toolName): ?string\n {\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n public function generateDocument(string $prompt, string $toolName, ?array $options = []): ?string\n {\n // Document is text-like for DeepSeek\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n private function requestDeepSeek(string $prompt, string $toolName): ?string\n {\n $this->lastTextError = null;\n\n $userId = $this->getUserId();\n if (!$userId) {\n $this->lastTextError = 'User is not authenticated.';\n\n return null;\n }\n\n $payload = [\n 'model' => $this->model,\n 'messages' => [\n ['role' => 'system', 'content' => 'You are a helpful AI assistant that generates structured educational content.'],\n ['role' => 'user', 'content' => $prompt],\n ],\n 'temperature' => $this->temperature,\n 'max_tokens' => $this->maxTokens,\n ];\n\n try {\n $response = $this->httpClient->request('POST', $this->apiUrl, [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $statusCode = $response->getStatusCode();\n $data = $response->toArray(false);\n\n if (\\is_array($data) && isset($data['error'])) {\n $msg = $data['error']['message'] ?? 'DeepSeek returned an error response.';\n $msg = \\is_string($msg) ? trim($msg) : 'DeepSeek returned an error response.';\n $this->lastTextError = 'HTTP '.$statusCode.': '.$msg;\n error_log('[AI][DeepSeek] Error response: '.$msg);\n\n return null;\n }\n\n $generatedContent = $data['choices'][0]['message']['content'] ?? null;\n if (!\\is_string($generatedContent) || '' === trim($generatedContent)) {\n $this->lastTextError = 'HTTP '.$statusCode.': Empty content returned.';\n error_log('[AI][DeepSeek] Empty content returned.');\n\n return null;\n }\n\n $this->saveAiRequest(\n $userId,\n $toolName,\n $prompt,\n 'deepseek',\n (int) ($data['usage']['prompt_tokens'] ?? 0),\n (int) ($data['usage']['completion_tokens'] ?? 0),\n (int) ($data['usage']['total_tokens'] ?? 0)\n );\n\n return $generatedContent;\n } catch (Exception $e) {\n $this->lastTextError = $e->getMessage();\n error_log('[AI][DeepSeek] Exception: '.$e->getMessage());\n\n return null;\n }\n }\n\n private function buildTextFailureMessage(string $provider): string\n {\n $detail = trim((string) $this->lastTextError);\n if ('' === $detail) {\n return $provider.' failed to generate the course structure.';\n }\n\n return $provider.' failed to generate the course structure: '.mb_substr($detail, 0, 1000);\n }\n\n private function filterValidAikenQuestions(string $quizContent): array\n {\n $questions = preg_split('/\\n{2,}/', trim($quizContent)) ?: [];\n\n $validQuestions = [];\n foreach ($questions as $questionBlock) {\n $lines = explode(\"\\n\", trim($questionBlock));\n\n if (\\count($lines) < 6) {\n continue;\n }\n\n $options = \\array_slice($lines, 1, 4);\n $validOptions = array_filter($options, static fn ($line) => (bool) preg_match('/^[A-D]\\. .+/', $line));\n\n $answerLine = (string) end($lines);\n if (4 === \\count($validOptions) && preg_match('/^ANSWER: [A-D]$/', $answerLine)) {\n $validQuestions[] = implode(\"\\n\", $lines);\n }\n }\n\n return $validQuestions;\n }\n\n /**\n * @param array<int, array{role:string,content:string}> $messages\n *\n * @return array<int, array{role:string,content:string}>\n */\n private function normalizeChatMessages(array $messages): array\n {\n $out = [];\n\n foreach ($messages as $m) {\n if (!\\is_array($m)) {\n continue;\n }\n\n $role = isset($m['role']) ? trim((string) $m['role']) : '';\n $content = isset($m['content']) ? trim((string) $m['content']) : '';\n\n if ('' === $role || '' === $content) {\n continue;\n }\n\n $role = strtolower($role);\n if (!\\in_array($role, ['system', 'user', 'assistant', 'tool'], true)) {\n $role = 'user';\n }\n\n $out[] = [\n 'role' => $role,\n 'content' => $content,\n ];\n }\n\n return $out;\n }\n\n /**\n * @param array<int, array{role:string,content:string}> $messages\n */\n private function messagesForLog(array $messages, int $maxChars = 900): string\n {\n $parts = [];\n foreach ($messages as $m) {\n $role = $m['role'] ?? 'user';\n $content = trim((string) ($m['content'] ?? ''));\n\n if ('' === $content) {\n continue;\n }\n\n $parts[] = strtoupper((string) $role).': '.mb_substr($content, 0, 300);\n }\n\n $s = implode(' | ', $parts);\n\n return mb_substr($s, 0, $maxChars);\n }\n\n /**\n * Resolve per-request overrides for text/chat.\n *\n * @param array<string,mixed> $options\n *\n * @return array{url:string,model:string,temperature:float,max_tokens:int}\n */\n private function resolveTextOptions(array $options): array\n {\n $url = (string) (($options['url'] ?? null) ?? $this->apiUrl);\n $model = (string) (($options['model'] ?? null) ?? $this->model);\n\n $temperature = (float) (($options['temperature'] ?? null) ?? $this->temperature);\n\n // Accept either max_tokens or max_output_tokens (normalize to max_tokens).\n $maxTokens = $options['max_tokens'] ?? ($options['max_output_tokens'] ?? null);\n $maxTokens = (int) (($maxTokens ?? null) ?? $this->maxTokens);\n\n if ($maxTokens <= 0) {\n $maxTokens = $this->maxTokens > 0 ? $this->maxTokens : 1000;\n }\n\n return [\n 'url' => $url,\n 'model' => $model,\n 'temperature' => $temperature,\n 'max_tokens' => $maxTokens,\n ];\n }\n\n private function saveAiRequest(\n int $userId,\n string $toolName,\n string $requestText,\n string $provider,\n int $promptTokens = 0,\n int $completionTokens = 0,\n int $totalTokens = 0\n ): void {\n try {\n $aiRequest = new AiRequests();\n $aiRequest\n ->setUserId($userId)\n ->setToolName($toolName)\n ->setRequestText($requestText)\n ->setPromptTokens($promptTokens)\n ->setCompletionTokens($completionTokens)\n ->setTotalTokens($totalTokens)\n ->setAiProvider($provider)\n ;\n\n $this->aiRequestsRepository->save($aiRequest);\n } catch (Exception $e) {\n error_log('[AI] Failed to save AiRequests record: '.$e->getMessage());\n }\n }\n\n private function getUserId(): ?int\n {\n $user = $this->security->getUser();\n\n return $user instanceof UserInterface ? $user->getId() : null;\n }\n}\n"}, "7be3801d89109c24b38b749485770909a4ea5bd3": {"t": 1785760145.7478826, "n": 16351, "h": "a10f6b022c682809", "c": "<?php\n\ndeclare(strict_types=1);\n\n/* For licensing terms, see /license.txt */\n\nnamespace Chamilo\\CoreBundle\\AiProvider;\n\nuse Chamilo\\CoreBundle\\Entity\\AiRequests;\nuse Chamilo\\CoreBundle\\Repository\\AiRequestsRepository;\nuse Chamilo\\CoreBundle\\Settings\\SettingsManager;\nuse Exception;\nuse RuntimeException;\nuse Symfony\\Bundle\\SecurityBundle\\Security;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass DeepSeekProvider implements AiProviderInterface, AiDocumentProviderInterface\n{\n private array $providerConfig;\n private string $apiUrl;\n private string $apiKey;\n private string $model;\n private float $temperature;\n private int $maxTokens;\n\n public function __construct(\n private readonly HttpClientInterface $httpClient,\n SettingsManager $settingsManager,\n private readonly AiRequestsRepository $aiRequestsRepository,\n private readonly Security $security\n ) {\n $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);\n $config = \\is_string($configJson) ? (json_decode($configJson, true) ?? []) : (\\is_array($configJson) ? $configJson : []);\n\n if (!isset($config['deepseek']) || !\\is_array($config['deepseek'])) {\n throw new RuntimeException('DeepSeek configuration is missing.');\n }\n\n $this->providerConfig = $config['deepseek'];\n $this->apiKey = (string) ($this->providerConfig['api_key'] ?? '');\n\n if ('' === trim($this->apiKey)) {\n throw new RuntimeException('DeepSeek API key is missing.');\n }\n\n $textCfg = $this->providerConfig['text'] ?? [];\n if (!\\is_array($textCfg)) {\n $textCfg = [];\n }\n\n $this->apiUrl = (string) ($textCfg['url'] ?? 'https://api.deepseek.com/chat/completions');\n $this->model = (string) ($textCfg['model'] ?? 'deepseek-chat');\n $this->temperature = (float) ($textCfg['temperature'] ?? 0.7);\n $this->maxTokens = (int) ($textCfg['max_tokens'] ?? 1000);\n }\n\n /**\n * Chat-style entrypoint (OpenAI-compatible).\n *\n * @param array<int, array{role:string,content:string}> $messages\n * @param array<string,mixed> $options\n */\n public function chat(array $messages, array $options = []): string\n {\n $userId = $this->getUserId();\n if (!$userId) {\n error_log('[AI][DeepSeek][chat] User not authenticated.');\n\n return 'Error: User is not authenticated.';\n }\n\n $normalized = $this->normalizeChatMessages($messages);\n if (empty($normalized)) {\n error_log('[AI][DeepSeek][chat] Empty messages payload.');\n\n return 'Error: Empty chat messages.';\n }\n\n $resolved = $this->resolveTextOptions($options);\n\n $payload = [\n 'model' => $resolved['model'],\n 'messages' => $normalized,\n 'temperature' => $resolved['temperature'],\n 'max_tokens' => $resolved['max_tokens'],\n ];\n\n // Optional safe overrides\n if (isset($options['top_p'])) {\n $payload['top_p'] = (float) $options['top_p'];\n }\n if (isset($options['presence_penalty'])) {\n $payload['presence_penalty'] = (float) $options['presence_penalty'];\n }\n if (isset($options['frequency_penalty'])) {\n $payload['frequency_penalty'] = (float) $options['frequency_penalty'];\n }\n\n try {\n $response = $this->httpClient->request('POST', $resolved['url'], [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $data = $response->toArray(false);\n\n if (\\is_array($data) && isset($data['error'])) {\n $msg = $data['error']['message'] ?? 'DeepSeek returned an error response.';\n $msg = \\is_string($msg) ? trim($msg) : 'DeepSeek returned an error response.';\n error_log('[AI][DeepSeek][chat] Error response: '.$msg);\n\n return 'Error: '.$msg;\n }\n\n $generated = $data['choices'][0]['message']['content'] ?? null;\n if (!\\is_string($generated) || '' === trim($generated)) {\n error_log('[AI][DeepSeek][chat] Empty content returned.');\n\n return 'Error: Empty response from DeepSeek.';\n }\n\n $this->saveAiRequest(\n $userId,\n 'chat',\n $this->messagesForLog($normalized, 900),\n 'deepseek',\n (int) ($data['usage']['prompt_tokens'] ?? 0),\n (int) ($data['usage']['completion_tokens'] ?? 0),\n (int) ($data['usage']['total_tokens'] ?? 0)\n );\n\n return trim($generated);\n } catch (Exception $e) {\n error_log('[AI][DeepSeek][chat] Exception: '.$e->getMessage());\n\n return 'Error: '.$e->getMessage();\n }\n }\n\n /**\n * Prompt-style entrypoint used by TaskGrader text mode and other features.\n *\n * @param array<string,mixed> $options\n */\n public function generateText(string $prompt, array $options = []): string\n {\n $prompt = trim($prompt);\n if ('' === $prompt) {\n return 'Error: Empty prompt.';\n }\n\n $messages = [\n ['role' => 'system', 'content' => 'You are a helpful assistant.'],\n ['role' => 'user', 'content' => $prompt],\n ];\n\n return $this->chat($messages, $options);\n }\n\n public function generateQuestions(string $topic, int $numQuestions, string $questionType, string $language): ?string\n {\n $prompt = \\sprintf(\n 'Generate %d \"%s\" questions in Aiken format in the %s language about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n The output should be plain text without additional symbols or markdown.',\n $numQuestions,\n $questionType,\n $language,\n $topic\n );\n\n return $this->requestDeepSeek($prompt, 'quiz');\n }\n\n public function generateLearnPath(string $topic, int $chaptersCount, string $language, int $wordsCount, bool $addTests, int $numQuestions): ?array\n {\n $tableOfContentsPrompt = \\sprintf(\n 'Generate a structured table of contents for a course in \"%s\" with %d chapters on \"%s\".\n Return a numbered list, each chapter on a new line. No conclusion.',\n $language,\n $chaptersCount,\n $topic\n );\n\n $lpStructure = $this->requestDeepSeek($tableOfContentsPrompt, 'learnpath');\n if (!$lpStructure) {\n return ['success' => false, 'message' => 'Failed to generate course structure.'];\n }\n\n $lpItems = [];\n $chapters = explode(\"\\n\", trim($lpStructure));\n foreach ($chapters as $chapterTitle) {\n $chapterTitle = trim($chapterTitle);\n if ('' === $chapterTitle) {\n continue;\n }\n\n $chapterPrompt = \\sprintf(\n 'Create a learning chapter in HTML for \"%s\" in \"%s\" with %d words.\n Title: \"%s\". Assume the reader already knows the context.',\n $topic,\n $language,\n $wordsCount,\n $chapterTitle\n );\n\n $chapterContent = $this->requestDeepSeek($chapterPrompt, 'learnpath');\n if (!$chapterContent) {\n continue;\n }\n\n $lpItems[] = [\n 'title' => $chapterTitle,\n 'content' => \"<html><head><title>{$chapterTitle}</title></head><body>{$chapterContent}</body></html>\",\n ];\n }\n\n $quizItems = [];\n if ($addTests) {\n foreach ($lpItems as $chapter) {\n $quizPrompt = \\sprintf(\n 'Generate %d multiple-choice questions in Aiken format in %s about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n Each question must have exactly 4 options and one answer line.\n Return only valid questions without extra text.',\n $numQuestions,\n $language,\n $chapter['title']\n );\n\n $quizContent = $this->requestDeepSeek($quizPrompt, 'learnpath');\n if ($quizContent) {\n $validQuestions = $this->filterValidAikenQuestions($quizContent);\n if (!empty($validQuestions)) {\n $quizItems[] = [\n 'title' => 'Quiz: '.$chapter['title'],\n 'content' => implode(\"\\n\\n\", $validQuestions),\n ];\n }\n }\n }\n }\n\n return [\n 'success' => true,\n 'topic' => $topic,\n 'lp_items' => $lpItems,\n 'quiz_items' => $quizItems,\n ];\n }\n\n public function gradeOpenAnswer(string $prompt, string $toolName): ?string\n {\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n public function generateDocument(string $prompt, string $toolName, ?array $options = []): ?string\n {\n // Document is text-like for DeepSeek\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n private function requestDeepSeek(string $prompt, string $toolName): ?string\n {\n $userId = $this->getUserId();\n if (!$userId) {\n throw new RuntimeException('User not authenticated.');\n }\n\n $payload = [\n 'model' => $this->model,\n 'messages' => [\n ['role' => 'system', 'content' => 'You are a helpful AI assistant that generates structured educational content.'],\n ['role' => 'user', 'content' => $prompt],\n ],\n 'temperature' => $this->temperature,\n 'max_tokens' => $this->maxTokens,\n ];\n\n try {\n $response = $this->httpClient->request('POST', $this->apiUrl, [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $data = $response->toArray(false);\n\n if (\\is_array($data) && isset($data['error'])) {\n $msg = $data['error']['message'] ?? 'DeepSeek returned an error response.';\n $msg = \\is_string($msg) ? trim($msg) : 'DeepSeek returned an error response.';\n error_log('[AI][DeepSeek] Error response: '.$msg);\n\n return null;\n }\n\n $generatedContent = $data['choices'][0]['message']['content'] ?? null;\n if (!\\is_string($generatedContent) || '' === trim($generatedContent)) {\n error_log('[AI][DeepSeek] Empty content returned.');\n\n return null;\n }\n\n $this->saveAiRequest(\n $userId,\n $toolName,\n $prompt,\n 'deepseek',\n (int) ($data['usage']['prompt_tokens'] ?? 0),\n (int) ($data['usage']['completion_tokens'] ?? 0),\n (int) ($data['usage']['total_tokens'] ?? 0)\n );\n\n return $generatedContent;\n } catch (Exception $e) {\n error_log('[AI][DeepSeek] Exception: '.$e->getMessage());\n\n return null;\n }\n }\n\n private function filterValidAikenQuestions(string $quizContent): array\n {\n $questions = preg_split('/\\n{2,}/', trim($quizContent)) ?: [];\n\n $validQuestions = [];\n foreach ($questions as $questionBlock) {\n $lines = explode(\"\\n\", trim($questionBlock));\n\n if (\\count($lines) < 6) {\n continue;\n }\n\n $options = \\array_slice($lines, 1, 4);\n $validOptions = array_filter($options, static fn ($line) => (bool) preg_match('/^[A-D]\\. .+/', $line));\n\n $answerLine = (string) end($lines);\n if (4 === \\count($validOptions) && preg_match('/^ANSWER: [A-D]$/', $answerLine)) {\n $validQuestions[] = implode(\"\\n\", $lines);\n }\n }\n\n return $validQuestions;\n }\n\n /**\n * @param array<int, array{role:string,content:string}> $messages\n *\n * @return array<int, array{role:string,content:string}>\n */\n private function normalizeChatMessages(array $messages): array\n {\n $out = [];\n\n foreach ($messages as $m) {\n if (!\\is_array($m)) {\n continue;\n }\n\n $role = isset($m['role']) ? trim((string) $m['role']) : '';\n $content = isset($m['content']) ? trim((string) $m['content']) : '';\n\n if ('' === $role || '' === $content) {\n continue;\n }\n\n $role = strtolower($role);\n if (!\\in_array($role, ['system', 'user', 'assistant', 'tool'], true)) {\n $role = 'user';\n }\n\n $out[] = [\n 'role' => $role,\n 'content' => $content,\n ];\n }\n\n return $out;\n }\n\n /**\n * @param array<int, array{role:string,content:string}> $messages\n */\n private function messagesForLog(array $messages, int $maxChars = 900): string\n {\n $parts = [];\n foreach ($messages as $m) {\n $role = $m['role'] ?? 'user';\n $content = trim((string) ($m['content'] ?? ''));\n\n if ('' === $content) {\n continue;\n }\n\n $parts[] = strtoupper((string) $role).': '.mb_substr($content, 0, 300);\n }\n\n $s = implode(' | ', $parts);\n\n return mb_substr($s, 0, $maxChars);\n }\n\n /**\n * Resolve per-request overrides for text/chat.\n *\n * @param array<string,mixed> $options\n *\n * @return array{url:string,model:string,temperature:float,max_tokens:int}\n */\n private function resolveTextOptions(array $options): array\n {\n $url = (string) (($options['url'] ?? null) ?? $this->apiUrl);\n $model = (string) (($options['model'] ?? null) ?? $this->model);\n\n $temperature = (float) (($options['temperature'] ?? null) ?? $this->temperature);\n\n // Accept either max_tokens or max_output_tokens (normalize to max_tokens).\n $maxTokens = $options['max_tokens'] ?? ($options['max_output_tokens'] ?? null);\n $maxTokens = (int) (($maxTokens ?? null) ?? $this->maxTokens);\n\n if ($maxTokens <= 0) {\n $maxTokens = $this->maxTokens > 0 ? $this->maxTokens : 1000;\n }\n\n return [\n 'url' => $url,\n 'model' => $model,\n 'temperature' => $temperature,\n 'max_tokens' => $maxTokens,\n ];\n }\n\n private function saveAiRequest(\n int $userId,\n string $toolName,\n string $requestText,\n string $provider,\n int $promptTokens = 0,\n int $completionTokens = 0,\n int $totalTokens = 0\n ): void {\n try {\n $aiRequest = new AiRequests();\n $aiRequest\n ->setUserId($userId)\n ->setToolName($toolName)\n ->setRequestText($requestText)\n ->setPromptTokens($promptTokens)\n ->setCompletionTokens($completionTokens)\n ->setTotalTokens($totalTokens)\n ->setAiProvider($provider)\n ;\n\n $this->aiRequestsRepository->save($aiRequest);\n } catch (Exception $e) {\n error_log('[AI] Failed to save AiRequests record: '.$e->getMessage());\n }\n }\n\n private function getUserId(): ?int\n {\n $user = $this->security->getUser();\n\n return $user instanceof UserInterface ? $user->getId() : null;\n }\n}\n"}, "acff39577cba08294fdb3121c09f6ff1e0da9cac": {"t": 1785760148.584394, "n": 9814, "h": "08df41f6034ac329", "c": "<?php\n\ndeclare(strict_types=1);\n\n/* For licensing terms, see /license.txt */\n\nnamespace Chamilo\\CoreBundle\\AiProvider;\n\nuse Chamilo\\CoreBundle\\Entity\\AiRequests;\nuse Chamilo\\CoreBundle\\Repository\\AiRequestsRepository;\nuse Chamilo\\CoreBundle\\Settings\\SettingsManager;\nuse Exception;\nuse RuntimeException;\nuse Symfony\\Bundle\\SecurityBundle\\Security;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass DeepSeekProvider implements AiProviderInterface, AiDocumentProviderInterface\n{\n private array $providerConfig;\n private string $apiUrl;\n private string $apiKey;\n private string $model;\n private float $temperature;\n private int $maxTokens;\n\n public function __construct(\n private readonly HttpClientInterface $httpClient,\n SettingsManager $settingsManager,\n private readonly AiRequestsRepository $aiRequestsRepository,\n private readonly Security $security\n ) {\n $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);\n $config = \\is_string($configJson) ? (json_decode($configJson, true) ?? []) : (\\is_array($configJson) ? $configJson : []);\n\n if (!isset($config['deepseek']) || !\\is_array($config['deepseek'])) {\n throw new RuntimeException('DeepSeek configuration is missing.');\n }\n\n $this->providerConfig = $config['deepseek'];\n $this->apiKey = (string) ($this->providerConfig['api_key'] ?? '');\n\n if ('' === trim($this->apiKey)) {\n throw new RuntimeException('DeepSeek API key is missing.');\n }\n\n $textCfg = $this->providerConfig['text'] ?? [];\n if (!\\is_array($textCfg)) {\n $textCfg = [];\n }\n\n $this->apiUrl = (string) ($textCfg['url'] ?? 'https://api.deepseek.com/chat/completions');\n $this->model = (string) ($textCfg['model'] ?? 'deepseek-chat');\n $this->temperature = (float) ($textCfg['temperature'] ?? 0.7);\n $this->maxTokens = (int) ($textCfg['max_tokens'] ?? 1000);\n }\n\n public function generateQuestions(string $topic, int $numQuestions, string $questionType, string $language): ?string\n {\n $prompt = \\sprintf(\n 'Generate %d \"%s\" questions in Aiken format in the %s language about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n The output should be plain text without additional symbols or markdown.',\n $numQuestions,\n $questionType,\n $language,\n $topic\n );\n\n return $this->requestDeepSeek($prompt, 'quiz');\n }\n\n public function generateLearnPath(string $topic, int $chaptersCount, string $language, int $wordsCount, bool $addTests, int $numQuestions): ?array\n {\n $tableOfContentsPrompt = \\sprintf(\n 'Generate a structured table of contents for a course in \"%s\" with %d chapters on \"%s\".\n Return a numbered list, each chapter on a new line. No conclusion.',\n $language,\n $chaptersCount,\n $topic\n );\n\n $lpStructure = $this->requestDeepSeek($tableOfContentsPrompt, 'learnpath');\n if (!$lpStructure) {\n return ['success' => false, 'message' => 'Failed to generate course structure.'];\n }\n\n $lpItems = [];\n $chapters = explode(\"\\n\", trim($lpStructure));\n foreach ($chapters as $chapterTitle) {\n $chapterTitle = trim($chapterTitle);\n if ('' === $chapterTitle) {\n continue;\n }\n\n $chapterPrompt = \\sprintf(\n 'Create a learning chapter in HTML for \"%s\" in \"%s\" with %d words.\n Title: \"%s\". Assume the reader already knows the context.',\n $topic,\n $language,\n $wordsCount,\n $chapterTitle\n );\n\n $chapterContent = $this->requestDeepSeek($chapterPrompt, 'learnpath');\n if (!$chapterContent) {\n continue;\n }\n\n $lpItems[] = [\n 'title' => $chapterTitle,\n 'content' => \"<html><head><title>{$chapterTitle}</title></head><body>{$chapterContent}</body></html>\",\n ];\n }\n\n $quizItems = [];\n if ($addTests) {\n foreach ($lpItems as $chapter) {\n $quizPrompt = \\sprintf(\n 'Generate %d multiple-choice questions in Aiken format in %s about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n Each question must have exactly 4 options and one answer line.\n Return only valid questions without extra text.',\n $numQuestions,\n $language,\n $chapter['title']\n );\n\n $quizContent = $this->requestDeepSeek($quizPrompt, 'learnpath');\n if ($quizContent) {\n $validQuestions = $this->filterValidAikenQuestions($quizContent);\n if (!empty($validQuestions)) {\n $quizItems[] = [\n 'title' => 'Quiz: '.$chapter['title'],\n 'content' => implode(\"\\n\\n\", $validQuestions),\n ];\n }\n }\n }\n }\n\n return [\n 'success' => true,\n 'topic' => $topic,\n 'lp_items' => $lpItems,\n 'quiz_items' => $quizItems,\n ];\n }\n\n public function gradeOpenAnswer(string $prompt, string $toolName): ?string\n {\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n public function generateDocument(string $prompt, string $toolName, ?array $options = []): ?string\n {\n // Document is text-like for DeepSeek\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n private function requestDeepSeek(string $prompt, string $toolName): ?string\n {\n $userId = $this->getUserId();\n if (!$userId) {\n throw new RuntimeException('User not authenticated.');\n }\n\n $payload = [\n 'model' => $this->model,\n 'messages' => [\n ['role' => 'system', 'content' => 'You are a helpful AI assistant that generates structured educational content.'],\n ['role' => 'user', 'content' => $prompt],\n ],\n 'temperature' => $this->temperature,\n 'max_tokens' => $this->maxTokens,\n ];\n\n try {\n $response = $this->httpClient->request('POST', $this->apiUrl, [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $data = $response->toArray(false);\n\n $generatedContent = $data['choices'][0]['message']['content'] ?? null;\n if (!\\is_string($generatedContent) || '' === trim($generatedContent)) {\n error_log('[AI][DeepSeek] Empty content returned.');\n\n return null;\n }\n\n $this->saveAiRequest(\n $userId,\n $toolName,\n $prompt,\n 'deepseek',\n (int) ($data['usage']['prompt_tokens'] ?? 0),\n (int) ($data['usage']['completion_tokens'] ?? 0),\n (int) ($data['usage']['total_tokens'] ?? 0)\n );\n\n return $generatedContent;\n } catch (Exception $e) {\n error_log('[AI][DeepSeek] Exception: '.$e->getMessage());\n\n return null;\n }\n }\n\n private function filterValidAikenQuestions(string $quizContent): array\n {\n $questions = preg_split('/\\n{2,}/', trim($quizContent));\n\n $validQuestions = [];\n foreach ($questions as $questionBlock) {\n $lines = explode(\"\\n\", trim($questionBlock));\n\n if (\\count($lines) < 6) {\n continue;\n }\n\n $options = \\array_slice($lines, 1, 4);\n $validOptions = array_filter($options, static fn ($line) => (bool) preg_match('/^[A-D]\\. .+/', $line));\n\n $answerLine = end($lines);\n if (4 === \\count($validOptions) && \\is_string($answerLine) && preg_match('/^ANSWER: [A-D]$/', $answerLine)) {\n $validQuestions[] = implode(\"\\n\", $lines);\n }\n }\n\n return $validQuestions;\n }\n\n private function saveAiRequest(\n int $userId,\n string $toolName,\n string $requestText,\n string $provider,\n int $promptTokens = 0,\n int $completionTokens = 0,\n int $totalTokens = 0\n ): void {\n try {\n $aiRequest = new AiRequests();\n $aiRequest\n ->setUserId($userId)\n ->setToolName($toolName)\n ->setRequestText($requestText)\n ->setPromptTokens($promptTokens)\n ->setCompletionTokens($completionTokens)\n ->setTotalTokens($totalTokens)\n ->setAiProvider($provider)\n ;\n\n $this->aiRequestsRepository->save($aiRequest);\n } catch (Exception $e) {\n error_log('[AI] Failed to save AiRequests record: '.$e->getMessage());\n }\n }\n\n private function getUserId(): ?int\n {\n $user = $this->security->getUser();\n\n return $user instanceof UserInterface ? $user->getId() : null;\n }\n}\n"}, "6b06f363a4207a58bcbee1cb805317d6a0d4962c": {"t": 1785760149.749689, "n": 9811, "h": "37c3b7479fcf7ada", "c": "<?php\n\ndeclare(strict_types=1);\n\n/* For licensing terms, see /license.txt */\n\nnamespace Chamilo\\CoreBundle\\AiProvider;\n\nuse Chamilo\\CoreBundle\\Entity\\AiRequests;\nuse Chamilo\\CoreBundle\\Repository\\AiRequestsRepository;\nuse Chamilo\\CoreBundle\\Settings\\SettingsManager;\nuse Exception;\nuse RuntimeException;\nuse Symfony\\Bundle\\SecurityBundle\\Security;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass DeepSeekProvider implements AiProviderInterface, AiDocumentProviderInterface\n{\n private array $providerConfig;\n private string $apiUrl;\n private string $apiKey;\n private string $model;\n private float $temperature;\n private int $maxTokens;\n\n public function __construct(\n private readonly HttpClientInterface $httpClient,\n SettingsManager $settingsManager,\n private readonly AiRequestsRepository $aiRequestsRepository,\n private readonly Security $security\n ) {\n $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);\n $config = \\is_string($configJson) ? (json_decode($configJson, true) ?? []) : (\\is_array($configJson) ? $configJson : []);\n\n if (!isset($config['deepseek']) || !\\is_array($config['deepseek'])) {\n throw new RuntimeException('DeepSeek configuration is missing.');\n }\n\n $this->providerConfig = $config['deepseek'];\n $this->apiKey = (string) ($this->providerConfig['api_key'] ?? '');\n\n if ('' === trim($this->apiKey)) {\n throw new RuntimeException('DeepSeek API key is missing.');\n }\n\n $textCfg = $this->providerConfig['text'] ?? [];\n if (!\\is_array($textCfg)) {\n $textCfg = [];\n }\n\n $this->apiUrl = (string) ($textCfg['url'] ?? 'https://api.deepseek.com/chat/completions');\n $this->model = (string) ($textCfg['model'] ?? 'deepseek-chat');\n $this->temperature = (float) ($textCfg['temperature'] ?? 0.7);\n $this->maxTokens = (int) ($textCfg['max_tokens'] ?? 1000);\n }\n\n public function generateQuestions(string $topic, int $numQuestions, string $questionType, string $language): ?string\n {\n $prompt = \\sprintf(\n 'Generate %d \"%s\" questions in Aiken format in the %s language about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n The output should be plain text without additional symbols or markdown.',\n $numQuestions,\n $questionType,\n $language,\n $topic\n );\n\n return $this->requestDeepSeek($prompt, 'quiz');\n }\n\n public function generateLearnPath(string $topic, int $chaptersCount, string $language, int $wordsCount, bool $addTests, int $numQuestions): ?array\n {\n $tableOfContentsPrompt = \\sprintf(\n 'Generate a structured table of contents for a course in \"%s\" with %d chapters on \"%s\".\n Return a numbered list, each chapter on a new line. No conclusion.',\n $language,\n $chaptersCount,\n $topic\n );\n\n $lpStructure = $this->requestDeepSeek($tableOfContentsPrompt, 'learnpath');\n if (!$lpStructure) {\n return ['success' => false, 'message' => 'Failed to generate course structure.'];\n }\n\n $lpItems = [];\n $chapters = explode(\"\\n\", trim($lpStructure));\n foreach ($chapters as $chapterTitle) {\n $chapterTitle = trim($chapterTitle);\n if ('' === $chapterTitle) {\n continue;\n }\n\n $chapterPrompt = \\sprintf(\n 'Create a learning chapter in HTML for \"%s\" in \"%s\" with %d words.\n Title: \"%s\". Assume the reader already knows the context.',\n $topic,\n $language,\n $wordsCount,\n $chapterTitle\n );\n\n $chapterContent = $this->requestDeepSeek($chapterPrompt, 'learnpath');\n if (!$chapterContent) {\n continue;\n }\n\n $lpItems[] = [\n 'title' => $chapterTitle,\n 'content' => \"<html><head><title>{$chapterTitle}</title></head><body>{$chapterContent}</body></html>\",\n ];\n }\n\n $quizItems = [];\n if ($addTests) {\n foreach ($lpItems as $chapter) {\n $quizPrompt = \\sprintf(\n 'Generate %d multiple-choice questions in Aiken format in %s about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n Each question must have exactly 4 options and one answer line.\n Return only valid questions without extra text.',\n $numQuestions,\n $language,\n $chapter['title']\n );\n\n $quizContent = $this->requestDeepSeek($quizPrompt, 'learnpath');\n if ($quizContent) {\n $validQuestions = $this->filterValidAikenQuestions($quizContent);\n if (!empty($validQuestions)) {\n $quizItems[] = [\n 'title' => 'Quiz: '.$chapter['title'],\n 'content' => implode(\"\\n\\n\", $validQuestions),\n ];\n }\n }\n }\n }\n\n return [\n 'success' => true,\n 'topic' => $topic,\n 'lp_items' => $lpItems,\n 'quiz_items' => $quizItems,\n ];\n }\n\n public function gradeOpenAnswer(string $prompt, string $toolName): ?string\n {\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n public function generateDocument(string $prompt, string $toolName, ?array $options = []): ?string\n {\n // Document is text-like for DeepSeek\n return $this->requestDeepSeek($prompt, $toolName);\n }\n\n private function requestDeepSeek(string $prompt, string $toolName): ?string\n {\n $userId = $this->getUserId();\n if (!$userId) {\n throw new RuntimeException('User not authenticated.');\n }\n\n $payload = [\n 'model' => $this->model,\n 'messages' => [\n ['role' => 'system', 'content' => 'You are a helpful AI assistant that generates structured educational content.'],\n ['role' => 'user', 'content' => $prompt],\n ],\n 'temperature' => $this->temperature,\n 'max_tokens' => $this->maxTokens,\n ];\n\n try {\n $response = $this->httpClient->request('POST', $this->apiUrl, [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $data = $response->toArray(false);\n\n $generatedContent = $data['choices'][0]['message']['content'] ?? null;\n if (!\\is_string($generatedContent) || '' === trim($generatedContent)) {\n error_log('[AI][DeepSeek] Empty content returned.');\n return null;\n }\n\n $this->saveAiRequest(\n $userId,\n $toolName,\n $prompt,\n 'deepseek',\n (int) ($data['usage']['prompt_tokens'] ?? 0),\n (int) ($data['usage']['completion_tokens'] ?? 0),\n (int) ($data['usage']['total_tokens'] ?? 0)\n );\n\n return $generatedContent;\n } catch (Exception $e) {\n error_log('[AI][DeepSeek] Exception: '.$e->getMessage());\n return null;\n }\n }\n\n private function filterValidAikenQuestions(string $quizContent): array\n {\n $questions = preg_split('/\\n{2,}/', trim($quizContent));\n\n $validQuestions = [];\n foreach ($questions as $questionBlock) {\n $lines = explode(\"\\n\", trim($questionBlock));\n\n if (\\count($lines) < 6) {\n continue;\n }\n\n $options = \\array_slice($lines, 1, 4);\n $validOptions = array_filter($options, static fn ($line) => (bool) preg_match('/^[A-D]\\. .+/', $line));\n\n $answerLine = end($lines);\n if (4 === \\count($validOptions) && \\is_string($answerLine) && preg_match('/^ANSWER: [A-D]$/', $answerLine)) {\n $validQuestions[] = implode(\"\\n\", $lines);\n }\n }\n\n return $validQuestions;\n }\n\n private function saveAiRequest(\n int $userId,\n string $toolName,\n string $requestText,\n string $provider,\n int $promptTokens = 0,\n int $completionTokens = 0,\n int $totalTokens = 0\n ): void {\n try {\n $aiRequest = new AiRequests();\n $aiRequest\n ->setUserId($userId)\n ->setToolName($toolName)\n ->setRequestText($requestText)\n ->setPromptTokens($promptTokens)\n ->setCompletionTokens($completionTokens)\n ->setTotalTokens($totalTokens)\n ->setAiProvider($provider)\n ;\n\n $this->aiRequestsRepository->save($aiRequest);\n } catch (Exception $e) {\n error_log('[AI] Failed to save AiRequests record: '.$e->getMessage());\n }\n }\n\n private function getUserId(): ?int\n {\n $user = $this->security->getUser();\n return $user instanceof UserInterface ? $user->getId() : null;\n }\n}\n"}, "5941d62437947574017adfa543b9a5e9520312dc": {"t": 1785760152.333723, "n": 9264, "h": "b071849a46116156", "c": "<?php\n\ndeclare(strict_types=1);\n\n/* For licensing terms, see /license.txt */\n\nnamespace Chamilo\\CoreBundle\\AiProvider;\n\nuse Chamilo\\CoreBundle\\Entity\\AiRequests;\nuse Chamilo\\CoreBundle\\Repository\\AiRequestsRepository;\nuse Chamilo\\CoreBundle\\Settings\\SettingsManager;\nuse Exception;\nuse RuntimeException;\nuse Symfony\\Bundle\\SecurityBundle\\Security;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass DeepSeekProvider implements AiProviderInterface\n{\n private string $apiUrl;\n private string $apiKey;\n private string $model;\n private float $temperature;\n private string $organizationId;\n private int $monthlyTokenLimit;\n private HttpClientInterface $httpClient;\n private AiRequestsRepository $aiRequestsRepository;\n private Security $security;\n\n public function __construct(\n HttpClientInterface $httpClient,\n SettingsManager $settingsManager,\n AiRequestsRepository $aiRequestsRepository,\n Security $security\n ) {\n $this->httpClient = $httpClient;\n $this->aiRequestsRepository = $aiRequestsRepository;\n $this->security = $security;\n\n // Get AI providers from settings\n $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);\n $config = json_decode($configJson, true) ?? [];\n\n if (!isset($config['deepseek'])) {\n throw new RuntimeException('DeepSeek configuration is missing.');\n }\n if (!isset($config['deepseek']['text'])) {\n throw new RuntimeException('DeepSeek configuration for text processing is missing.');\n }\n\n $this->apiKey = $config['deepseek']['api_key'] ?? '';\n $this->organizationId = $config['deepseek']['organization_id'] ?? '';\n $this->monthlyTokenLimit = $config['deepseek']['monthly_token_limit'] ?? 1000;\n $this->apiUrl = $config['deepseek']['text']['url'] ?? 'https://api.deepseek.com/chat/completions';\n $this->model = $config['deepseek']['text']['model'] ?? 'deepseek-chat';\n $this->temperature = $config['deepseek']['text']['temperature'] ?? 0.7;\n\n if (empty($this->apiKey)) {\n throw new RuntimeException('DeepSeek API key is missing.');\n }\n }\n\n public function generateQuestions(string $topic, int $numQuestions, string $questionType, string $language): ?string\n {\n $prompt = \\sprintf(\n 'Generate %d \"%s\" questions in Aiken format in the %s language about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n The output should be plain text without additional symbols or markdown.',\n $numQuestions,\n $questionType,\n $language,\n $topic\n );\n\n return $this->requestDeepSeekAI($prompt, 'quiz');\n }\n\n public function generateLearnPath(string $topic, int $chaptersCount, string $language, int $wordsCount, bool $addTests, int $numQuestions): ?array\n {\n // Step 1: Generate the Table of Contents\n $tableOfContentsPrompt = \\sprintf(\n 'Generate a structured table of contents for a course in \"%s\" with %d chapters on \"%s\".\n Return a numbered list, each chapter on a new line. No conclusion.',\n $language,\n $chaptersCount,\n $topic\n );\n\n $lpStructure = $this->requestDeepSeekAI($tableOfContentsPrompt, 'learnpath');\n if (!$lpStructure) {\n return ['success' => false, 'message' => 'Failed to generate course structure.'];\n }\n\n // Step 2: Generate content for each chapter\n $lpItems = [];\n $chapters = explode(\"\\n\", trim($lpStructure));\n foreach ($chapters as $index => $chapterTitle) {\n $chapterTitle = trim($chapterTitle);\n if (empty($chapterTitle)) {\n continue;\n }\n\n $chapterPrompt = \\sprintf(\n 'Create a learning chapter in HTML for \"%s\" in \"%s\" with %d words.\n Title: \"%s\". Assume the reader already knows the context.',\n $topic,\n $language,\n $wordsCount,\n $chapterTitle\n );\n\n $chapterContent = $this->requestDeepSeekAI($chapterPrompt, 'learnpath');\n if (!$chapterContent) {\n continue;\n }\n\n $lpItems[] = [\n 'title' => $chapterTitle,\n 'content' => \"<html><head><title>{$chapterTitle}</title></head><body>{$chapterContent}</body></html>\",\n ];\n }\n\n // Step 3: Generate quizzes if enabled\n $quizItems = [];\n if ($addTests) {\n foreach ($lpItems as &$chapter) {\n $quizPrompt = \\sprintf(\n 'Generate %d multiple-choice questions in Aiken format in %s about \"%s\".\n Ensure each question follows this format:\n\n 1. The question text.\n A. Option A\n B. Option B\n C. Option C\n D. Option D\n ANSWER: (Correct answer letter)\n\n Each question must have exactly 4 options and one answer line.\n Return only valid questions without extra text.',\n $numQuestions,\n $language,\n $chapter['title']\n );\n\n $quizContent = $this->requestDeepSeekAI($quizPrompt, 'learnpath');\n\n if ($quizContent) {\n $validQuestions = $this->filterValidAikenQuestions($quizContent);\n\n if (!empty($validQuestions)) {\n $quizItems[] = [\n 'title' => 'Quiz: '.$chapter['title'],\n 'content' => implode(\"\\n\\n\", $validQuestions),\n ];\n }\n }\n }\n }\n\n return [\n 'success' => true,\n 'topic' => $topic,\n 'lp_items' => $lpItems,\n 'quiz_items' => $quizItems,\n ];\n }\n\n private function filterValidAikenQuestions(string $quizContent): array\n {\n $questions = preg_split('/\\n{2,}/', trim($quizContent));\n\n $validQuestions = [];\n foreach ($questions as $questionBlock) {\n $lines = explode(\"\\n\", trim($questionBlock));\n\n if (\\count($lines) < 6) {\n continue;\n }\n\n $options = \\array_slice($lines, 1, 4);\n $validOptions = array_filter($options, fn ($line) => preg_match('/^[A-D]\\. .+/', $line));\n\n $answerLine = end($lines);\n if (4 === \\count($validOptions) && preg_match('/^ANSWER: [A-D]$/', $answerLine)) {\n $validQuestions[] = implode(\"\\n\", $lines);\n }\n }\n\n return $validQuestions;\n }\n\n private function requestDeepSeekAI(string $prompt, string $toolName): ?string\n {\n $userId = $this->getUserId();\n if (!$userId) {\n throw new RuntimeException('User not authenticated.');\n }\n\n $payload = [\n 'model' => $this->model,\n 'messages' => [\n ['role' => 'system', 'content' => 'You are a helpful AI assistant that generates structured educational content.'],\n ['role' => 'user', 'content' => $prompt],\n ],\n 'temperature' => $this->temperature,\n 'max_tokens' => 300,\n ];\n\n try {\n $response = $this->httpClient->request('POST', $this->apiUrl, [\n 'headers' => [\n 'Authorization' => 'Bearer '.$this->apiKey,\n 'Content-Type' => 'application/json',\n ],\n 'json' => $payload,\n ]);\n\n $statusCode = $response->getStatusCode();\n $data = $response->toArray();\n\n if (200 === $statusCode && isset($data['choices'][0]['message']['content'])) {\n $generatedContent = $data['choices'][0]['message']['content'];\n\n $aiRequest = new AiRequests();\n $aiRequest->setUserId($userId)\n ->setToolName($toolName)\n ->setRequestText($prompt)\n ->setPromptTokens($data['usage']['prompt_tokens'] ?? 0)\n ->setCompletionTokens($data['usage']['completion_tokens'] ?? 0)\n ->setTotalTokens($data['usage']['total_tokens'] ?? 0)\n ->setAiProvider('deepseek')\n ;\n\n $this->aiRequestsRepository->save($aiRequest);\n\n return $generatedContent;\n }\n\n return null;\n } catch (Exception $e) {\n error_log('[AI][DeepSeek] Exception: '.$e->getMessage());\n\n return null;\n }\n }\n\n public function gradeOpenAnswer(string $prompt, string $toolName): ?string\n {\n return $this->requestDeepSeekAI($prompt, $toolName);\n }\n\n private function getUserId(): ?int\n {\n $user = $this->security->getUser();\n\n return $user instanceof UserInterface ? $user->getId() : null;\n }\n}\n"}}}}