ApiRepository::pushStatus()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Startwind\Forrest\Repository\Api;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\Exception\ClientException;
7
use GuzzleHttp\RequestOptions;
8
use Startwind\Forrest\Command\Answer\Answer;
9
use Startwind\Forrest\Command\Command;
10
use Startwind\Forrest\Command\CommandFactory;
11
use Startwind\Forrest\Command\Tool\Tool;
12
use Startwind\Forrest\Logger\ForrestLogger;
13
use Startwind\Forrest\Repository\QuestionAware;
14
use Startwind\Forrest\Repository\Repository;
15
use Startwind\Forrest\Repository\SearchAware;
16
use Startwind\Forrest\Repository\StatusAwareRepository;
17
use Startwind\Forrest\Repository\ToolAware;
18
use Startwind\Forrest\Util\OSHelper;
19
20
class ApiRepository implements Repository, SearchAware, ToolAware, StatusAwareRepository, QuestionAware
21
{
22
    public function __construct(
23
        protected readonly string $endpoint,
24
        private readonly string   $name,
25
        private readonly string   $description,
26
        protected readonly Client $client,
27
    )
28
    {
29
    }
30
31
    /**
32
     * @inheritDoc
33
     */
34
    public function getName(): string
35
    {
36
        return $this->name;
37
    }
38
39
    /**
40
     * @inheritDoc
41
     */
42
    public function getDescription(): string
43
    {
44
        return $this->description;
45
    }
46
47
    /**
48
     * @inheritDoc
49
     */
50
    public function isSpecial(): bool
51
    {
52
        return false;
53
    }
54
55
    /**
56
     * @todo merge the three search functions and remove duplicate code
57
     *
58
     * @inheritDoc
59
     */
60
    public function searchByFile(array $files): array
61
    {
62
        $payload = [
63
            'types' => $files
64
        ];
65
66
        $response = $this->client->post(
67
            $this->endpoint . 'search/file',
68
            [
69
                RequestOptions::JSON => $payload,
70
                'verify' => false
71
            ]
72
        );
73
74
        $plainCommands = json_decode($response->getBody(), true);
75
76
        $commandsArray = $plainCommands['commands'];
77
78
        $commands = [];
79
80
        foreach ($commandsArray as $commandsArrayElement) {
81
            $commands[$commandsArrayElement['name']] = CommandFactory::fromArray($commandsArrayElement);
82
        }
83
84
        return $commands;
85
    }
86
87
    /**
88
     * @inheritDoc
89
     */
90
    public function searchByPattern(array $patterns): array
91
    {
92
        $payload = [
93
            'patterns' => $patterns
94
        ];
95
96
        $response = $this->client->post(
97
            $this->endpoint . 'search/pattern',
98
            [
99
                RequestOptions::JSON => $payload,
100
                'verify' => false
101
            ]
102
        );
103
104
        $plainCommands = json_decode($response->getBody(), true);
105
106
        $commandsArray = $plainCommands['commands'];
107
108
        $commands = [];
109
110
        foreach ($commandsArray as $commandsArrayElement) {
111
            $commands[$commandsArrayElement['score'] . $commandsArrayElement['name']] = CommandFactory::fromArray($commandsArrayElement);
112
        }
113
114
        ksort($commands);
115
116
        return $commands;
117
    }
118
119
    /**
120
     * @inheritDoc
121
     */
122
    public function searchByTools(array $tools): array
123
    {
124
        $payload = [
125
            'tool' => $tools[0]
126
        ];
127
128
        $response = $this->client->post(
129
            $this->endpoint . 'search/tool',
130
            [
131
                RequestOptions::JSON => $payload,
132
                'verify' => false
133
            ]
134
        );
135
136
        $plainCommands = json_decode($response->getBody(), true);
137
138
        $commandsArray = $plainCommands['commands'];
139
140
        $commands = [];
141
142
        foreach ($commandsArray as $commandsArrayElement) {
143
            $commands[$commandsArrayElement['name']] = CommandFactory::fromArray($commandsArrayElement);
144
        }
145
146
        return $commands;
147
    }
148
149
    public function getCommand(string $identifier): Command
150
    {
151
        $response = $this->client->get($this->endpoint . 'command/' . urlencode($identifier), ['verify' => false]);
152
        $plainCommands = json_decode($response->getBody(), true);
153
154
        $command = CommandFactory::fromArray($plainCommands['command']);
155
156
        return $command;
157
    }
158
159
    public function assertHealth(): void
160
    {
161
        try {
162
            $this->client->get($this->endpoint . 'health', ['verify' => false]);
163
        } catch (\Exception $exception) {
164
            ForrestLogger::warn('Unable to connect to Forrest API ("' . $this->endpoint . '"): ' . $exception->getMessage());
165
            throw new \RuntimeException('Unable to connect to Forrest API ("' . $this->endpoint . '")');
166
        }
167
    }
168
169
    /**
170
     * @inheritDoc
171
     */
172
    public function findToolInformation(string $tool): Tool|bool
173
    {
174
        try {
175
            $response = $this->client->get($this->endpoint . 'tool/' . urlencode($tool), ['verify' => false]);
176
        } catch (ClientException $exception) {
177
            if ($exception->getResponse()->getStatusCode() == 404) {
178
                return false;
179
            }
180
181
            ForrestLogger::warn('Unable to get tool information. ' . $exception->getMessage());
182
            return false;
183
        }
184
185
        $information = json_decode((string)$response->getBody(), true);
186
187
        $toolInfo = new Tool($tool, $information['tool']['description']);
188
189
        if (array_key_exists('see', $information['tool'])) {
190
            $toolInfo->setSee($information['tool']['see']);
191
        }
192
193
        return $toolInfo;
194
    }
195
196
    public function ask(string $question): array
197
    {
198
        $payload = [
199
            'os' => OSHelper::getOS(),
200
            'question' => $question,
201
            'fromCli' => true
202
        ];
203
204
        try {
205
            $response = $this->client->post($this->endpoint . 'ai/ask', [RequestOptions::JSON => $payload, 'verify' => false]);
206
        } catch (ClientException $exception) {
207
            if ($exception->getResponse()->getStatusCode() == 404) {
208
                return [];
209
            }
210
            ForrestLogger::warn('Unable to ask:  ' . $exception->getMessage());
211
            return [];
212
        }
213
214
        $body = (string)$response->getBody();
215
        $information = json_decode($body, true);
216
217
        if (is_null($information)) {
218
            ForrestLogger::warn('Plain API result: ' . $body);
219
            throw new \RuntimeException('The API did not return valid JSON.');
220
        }
221
222
        if (!array_key_exists('answer', $information)) {
223
            ForrestLogger::warn('Plain API result: ' . $body);
224
            throw new \RuntimeException('The API did not provide the mandatory field "answer".');
225
        }
226
227
        $answer = $information['answer'];
228
229
        if (array_key_exists('commandArray', $answer) && count($answer['commandArray']) > 0) {
230
            $answers[] = new Answer($answer['commandArray'], $question, $answer['text'], $answer['commandArray']['name']);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$answers was never initialized. Although not strictly required by PHP, it is generally a good practice to add $answers = array(); before regardless.
Loading history...
231
        } else {
232
            $answers[] = new Answer($answer['command'], $question, $answer['text']);
233
        }
234
235
        return $answers;
236
    }
237
238
    public function explain(string $prompt): array
239
    {
240
        $payload = [
241
            'prompt' => $prompt
242
        ];
243
244
        try {
245
            $response = $this->client->post($this->endpoint . 'ai/explain', [RequestOptions::JSON => $payload, 'verify' => false]);
246
        } catch (ClientException $exception) {
247
            if ($exception->getResponse()->getStatusCode() == 404) {
248
                return [];
249
            }
250
            ForrestLogger::warn('Unable to explain:  ' . $exception->getMessage());
251
            return [];
252
        }
253
254
        $result = json_decode((string)$response->getBody(), true);
255
256
        $explanation = $result['explanation'];
257
258
        $answers[] = new Answer($prompt, $prompt, $explanation);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$answers was never initialized. Although not strictly required by PHP, it is generally a good practice to add $answers = array(); before regardless.
Loading history...
259
260
        return $answers;
261
    }
262
263
    /**
264
     * @inheritDoc
265
     */
266
    public function pushStatus(string $commandIdentifier, string $status): void
267
    {
268
        $this->client->post($this->endpoint . 'command/' . urlencode($commandIdentifier) . '/stats/' . urlencode($status), ['verify' => false]);
269
    }
270
}
271