Passed
Pull Request — master (#7304)
by Yannick
09:41
created

GrokDocumentProvider   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 132
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 71
c 1
b 0
f 0
dl 0
loc 132
rs 10
wmc 14

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 33 4
A generateDocument() 0 3 1
B requestGrokAI() 0 75 7
A getUserId() 0 3 2
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\Contracts\HttpClient\HttpClientInterface;
14
15
/**
16
 * Grok (xAI) provider for document generation.
17
 * Note: As of January 2026, document generation is not natively supported in the xAI API as binary files (e.g., PDF).
18
 * This implementation uses the text responses endpoint to generate structured text content (e.g., Markdown),
19
 * which can be post-processed into a document format client-side.
20
 */
21
class GrokDocumentProvider implements AiDocumentProviderInterface
22
{
23
    private string $apiUrl;
24
    private string $apiKey;
25
    private string $model;
26
    private array $defaultOptions;
27
    private HttpClientInterface $httpClient;
28
    private AiRequestsRepository $aiRequestsRepository;
29
    private SettingsManager $settingsManager;
0 ignored issues
show
introduced by
The private property $settingsManager is not used, and could be removed.
Loading history...
30
31
    public function __construct(
32
        HttpClientInterface $httpClient,
33
        AiRequestsRepository $aiRequestsRepository,
34
        SettingsManager $settingsManager,
35
        Security $security
36
    ) {
37
        $this->httpClient = $httpClient;
38
        $this->aiRequestsRepository = $aiRequestsRepository;
39
        $this->security = $security;
40
41
        // Get AI providers from settings
42
        $configJson = $settingsManager->getSetting('ai_helpers.ai_providers', true);
43
        $config = json_decode($configJson, true) ?? [];
44
45
        if (!isset($config['grok'])) {
46
            throw new RuntimeException('Grok configuration is missing.');
47
        }
48
        if (!isset($config['grok']['document'])) {
49
            throw new RuntimeException('Grok configuration for document generation is missing.');
50
        }
51
        $grokConfig = $config['grok'];
52
53
        $this->apiUrl = $grokConfig['url'] ?? 'https://api.x.ai/v1/responses';
54
        $this->apiKey = $grokConfig['api_key'] ?? '';
55
        $this->model = $grokConfig['model'] ?? 'grok-4-1-fast-reasoning';
56
57
        if (empty($this->apiKey)) {
58
            throw new RuntimeException('Grok API key is missing.');
59
        }
60
61
        $this->defaultOptions = [
62
            'temperature' => $grokConfig['temperature'] ?? 0.7,
63
            'format' => $grokConfig['format'] ?? 'pdf', // e.g., 'markdown', 'pdf' (for prompt instruction)
64
        ];
65
    }
66
67
    public function generateDocument(string $prompt, string $toolName, ?array $options = []): ?string
68
    {
69
        return $this->requestGrokAI($prompt, $toolName, $options);
70
    }
71
72
    private function requestGrokAI(string $prompt, string $toolName, array $options = []): ?string
73
    {
74
        $userId = $this->getUserId();
75
        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...
76
            throw new RuntimeException('User not authenticated.');
77
        }
78
79
        // Build system prompt to instruct document generation
80
        $systemContent = 'You are a helpful assistant that generates well-structured documents. Output in the specified format (e.g., Markdown for easy conversion to PDF).';
81
82
        // Append format instruction to user prompt if provided
83
        $userContent = $prompt;
84
        $format = $options['format'] ?? $this->defaultOptions['format'];
85
        if ($format) {
86
            $userContent .= "\n\nOutput the document in {$format} format.";
87
        }
88
89
        $payload = [
90
            'model' => $this->model,
91
            'input' => [
92
                ['role' => 'system', 'content' => $systemContent],
93
                ['role' => 'user', 'content' => $userContent],
94
            ],
95
            ...array_merge($this->defaultOptions, $options),
96
        ];
97
98
        try {
99
            $response = $this->httpClient->request('POST', $this->apiUrl, [
100
                'headers' => [
101
                    'Authorization' => 'Bearer '.$this->apiKey,
102
                    'Content-Type' => 'application/json',
103
                ],
104
                'json' => $payload,
105
            ]);
106
107
            $statusCode = $response->getStatusCode();
108
            if (200 !== $statusCode) {
109
                throw new RuntimeException('API request failed with status: ' . $statusCode);
110
            }
111
112
            $data = $response->toArray();
113
114
            // Check for error key first
115
            if (isset($data['error'])) {
116
                throw new RuntimeException('API error: ' . $data['error']['message']);
117
            }
118
119
            // Extract generated content from response structure
120
            if (isset($data['output'][0]['content'][0]['text'])) {
121
                $generatedContent = $data['output'][0]['content'][0]['text'];
122
123
                // Usage is available for text generation
124
                $usage = $data['usage'] ?? ['prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0];
125
126
                // Log request
127
                $aiRequest = new AiRequests();
128
                $aiRequest->setUserId($userId)
129
                    ->setToolName($toolName)
130
                    ->setRequestText($prompt)
131
                    ->setPromptTokens($usage['prompt_tokens'])
132
                    ->setCompletionTokens($usage['completion_tokens'])
133
                    ->setTotalTokens($usage['total_tokens'])
134
                    ->setAiProvider('grok')
135
                ;
136
137
                $this->aiRequestsRepository->save($aiRequest);
138
139
                return $generatedContent;
140
            }
141
142
            return null;
143
        } catch (Exception $e) {
144
            error_log('[AI][Grok] Exception: '.$e->getMessage());
145
146
            return null;
147
        }
148
    }
149
150
    private function getUserId(): ?int
151
    {
152
        return $this->user ? $this->user->getId() : null;
0 ignored issues
show
Bug Best Practice introduced by
The property user does not exist on Chamilo\CoreBundle\AiProvider\GrokDocumentProvider. Did you maybe forget to declare it?
Loading history...
153
    }
154
}
155