Passed
Push — main ( d01439...b93b89 )
by Nils
02:46
created

ApiRepository::explain()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 3
nop 1
dl 0
loc 23
rs 9.8333
c 0
b 0
f 0
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
            'fromCli' => true
200
        ];
201
202
        try {
203
            $response = $this->client->post($this->endpoint . 'ai/ask', [RequestOptions::JSON => $payload, 'verify' => false]);
204
        } catch (ClientException $exception) {
205
            if ($exception->getResponse()->getStatusCode() == 404) {
206
                return [];
207
            }
208
            ForrestLogger::warn('Unable to ask:  ' . $exception->getMessage());
209
            return [];
210
        }
211
212
        $body = (string)$response->getBody();
213
        $information = json_decode($body, true);
214
215
        if (is_null($information)) {
216
            ForrestLogger::warn('Plain API result: ' . $body);
217
            throw new \RuntimeException('The API did not return valid JSON.');
218
219
        }
220
221
        if (!array_key_exists('answer', $information)) {
222
            ForrestLogger::warn('Plain API result: ' . $body);
223
            throw new \RuntimeException('The API did not provide the mandatory field "answer".');
224
        }
225
226
        $answer = $information['answer'];
227
228
        if (array_key_exists('commandArray', $answer) && count($answer['commandArray']) > 0) {
229
            $answers[] = new Answer($answer['commandArray'], $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...
230
        } else {
231
            $answers[] = new Answer($answer['command'], $question, $answer['text']);
232
        }
233
234
        return $answers;
235
    }
236
237
    public function explain(string $prompt): array
238
    {
239
        $payload = [
240
            'prompt' => $prompt
241
        ];
242
243
        try {
244
            $response = $this->client->post($this->endpoint . 'ai/explain', [RequestOptions::JSON => $payload, 'verify' => false]);
245
        } catch (ClientException $exception) {
246
            if ($exception->getResponse()->getStatusCode() == 404) {
247
                return [];
248
            }
249
            ForrestLogger::warn('Unable to explain:  ' . $exception->getMessage());
250
            return [];
251
        }
252
253
        $result = json_decode((string)$response->getBody(), true);
254
255
        $explanation = $result['explanation'];
256
257
        $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...
258
259
        return $answers;
260
    }
261
262
    /**
263
     * @inheritDoc
264
     */
265
    public function pushStatus(string $commandIdentifier, string $status): void
266
    {
267
        $this->client->post($this->endpoint . 'command/' . urlencode($commandIdentifier) . '/stats/' . urlencode($status), ['verify' => false]);
268
    }
269
}
270