Passed
Pull Request — main (#148)
by Nils
03:04
created

ApiRepository::isSpecial()   A

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 0
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['name']] = CommandFactory::fromArray($commandsArrayElement);
112
        }
113
114
        return $commands;
115
    }
116
117
    /**
118
     * @inheritDoc
119
     */
120
    public function searchByTools(array $tools): array
121
    {
122
        $payload = [
123
            'tool' => $tools[0]
124
        ];
125
126
        $response = $this->client->post(
127
            $this->endpoint . 'search/tool',
128
            [
129
                RequestOptions::JSON => $payload,
130
                'verify' => false
131
            ]
132
        );
133
134
        $plainCommands = json_decode($response->getBody(), true);
135
136
        $commandsArray = $plainCommands['commands'];
137
138
        $commands = [];
139
140
        foreach ($commandsArray as $commandsArrayElement) {
141
            $commands[$commandsArrayElement['name']] = CommandFactory::fromArray($commandsArrayElement);
142
        }
143
144
        return $commands;
145
    }
146
147
    public function getCommand(string $identifier): Command
148
    {
149
        $response = $this->client->get($this->endpoint . 'command/' . urlencode($identifier), ['verify' => false]);
150
        $plainCommands = json_decode($response->getBody(), true);
151
152
        $command = CommandFactory::fromArray($plainCommands['command']);
153
154
        return $command;
155
    }
156
157
    public function assertHealth(): void
158
    {
159
        try {
160
            $this->client->get($this->endpoint . 'health', ['verify' => false]);
161
        } catch (\Exception $exception) {
162
            ForrestLogger::warn('Unable to connect to Forrest API ("' . $this->endpoint . '"): ' . $exception->getMessage());
163
            throw new \RuntimeException('Unable to connect to Forrest API ("' . $this->endpoint . '")');
164
        }
165
    }
166
167
    /**
168
     * @inheritDoc
169
     */
170
    public function findToolInformation(string $tool): Tool|bool
171
    {
172
        try {
173
            $response = $this->client->get($this->endpoint . 'tool/' . urlencode($tool), ['verify' => false]);
174
        } catch (ClientException $exception) {
175
            if ($exception->getResponse()->getStatusCode() == 404) {
176
                return false;
177
            }
178
179
            ForrestLogger::warn('Unable to get tool information. ' . $exception->getMessage());
180
            return false;
181
        }
182
183
        $information = json_decode((string)$response->getBody(), true);
184
185
        $toolInfo = new Tool($tool, $information['tool']['description']);
186
187
        if (array_key_exists('see', $information['tool'])) {
188
            $toolInfo->setSee($information['tool']['see']);
189
        }
190
191
        return $toolInfo;
192
    }
193
194
    public function ask(string $question): array
195
    {
196
        $payload = [
197
            'os' => OSHelper::getOS(),
198
            'question' => $question
199
        ];
200
201
        try {
202
            $response = $this->client->post($this->endpoint . 'ai/ask', [RequestOptions::JSON => $payload, 'verify' => false]);
203
        } catch (ClientException $exception) {
204
            if ($exception->getResponse()->getStatusCode() == 404) {
205
                return [];
206
            }
207
            ForrestLogger::warn('Unable to ask:  ' . $exception->getMessage());
208
            return [];
209
        }
210
211
        $body = (string)$response->getBody();
212
        $information = json_decode($body, true);
213
214
        if (is_null($information)) {
215
            ForrestLogger::warn('Plain API result: ' . $body);
216
            throw new \RuntimeException('The API did not return valid JSON.');
217
218
        }
219
220
        if (!array_key_exists('answer', $information)) {
221
            ForrestLogger::warn('Plain API result: ' . $body);
222
            throw new \RuntimeException('The API did not provide the mandatory field "answer".');
223
        }
224
225
        $answer = $information['answer'];
226
227
228
        $answers[] = new Answer($answer['command'], $question, $answer['text']);
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...
229
230
        return $answers;
231
    }
232
233
    public function explain(string $prompt): array
234
    {
235
        $payload = [
236
            'prompt' => $prompt
237
        ];
238
239
        try {
240
            $response = $this->client->post($this->endpoint . 'ai/explain', [RequestOptions::JSON => $payload, 'verify' => false]);
241
        } catch (ClientException $exception) {
242
            if ($exception->getResponse()->getStatusCode() == 404) {
243
                return [];
244
            }
245
            ForrestLogger::warn('Unable to explain:  ' . $exception->getMessage());
246
            return [];
247
        }
248
249
        $result = json_decode((string)$response->getBody(), true);
250
251
        $explanation = $result['explanation'];
252
253
        $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...
254
255
        return $answers;
256
    }
257
258
    /**
259
     * @inheritDoc
260
     */
261
    public function pushStatus(string $commandIdentifier, string $status): void
262
    {
263
        $this->client->post($this->endpoint . 'command/' . urlencode($commandIdentifier) . '/stats/' . urlencode($status), ['verify' => false]);
264
    }
265
}
266