PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.7.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.7.0
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / api.php
ai-engine / classes Last commit date
engines 1 year ago modules 1 year ago queries 1 year ago admin.php 2 years ago api.php 1 year ago core.php 1 year ago init.php 2 years ago logging.php 1 year ago reply.php 1 year ago rest.php 1 year ago
api.php
447 lines
1 <?php
2
3 class Meow_MWAI_API {
4 public $core;
5 private $chatbot_module;
6 private $discussions_module;
7 private $bearer_token;
8
9 public function __construct( $chatbot_module, $discussions_module ) {
10 global $mwai_core;
11 $this->core = $mwai_core;
12 $this->chatbot_module = $chatbot_module;
13 $this->discussions_module = $discussions_module;
14 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
15 }
16
17 #region REST API
18 function rest_api_init() {
19 $public_api = $this->core->get_option( 'public_api' );
20 if ( !$public_api ) { return; }
21 $this->bearer_token = $this->core->get_option( 'public_api_bearer_token' );
22 if ( !empty( $this->bearer_token ) ) {
23 add_filter( 'mwai_allow_public_api', [ $this, 'auth_via_bearer_token' ], 10, 3 );
24 }
25
26 register_rest_route( 'mwai/v1', '/simpleAuthCheck', array(
27 'methods' => 'GET',
28 'callback' => array( $this, 'rest_simpleAuthCheck' ),
29 'permission_callback' => function( $request ) {
30 return $this->core->can_access_public_api( 'simpleAuthCheck', $request );
31 },
32 ) );
33 register_rest_route( 'mwai/v1', '/simpleTextQuery', array(
34 'methods' => 'POST',
35 'callback' => array( $this, 'rest_simpleTextQuery' ),
36 'permission_callback' => function( $request ) {
37 return $this->core->can_access_public_api( 'simpleTextQuery', $request );
38 },
39 ) );
40 register_rest_route( 'mwai/v1', '/simpleImageQuery', array(
41 'methods' => 'POST',
42 'callback' => array( $this, 'rest_simpleImageQuery' ),
43 'permission_callback' => function( $request ) {
44 return $this->core->can_access_public_api( 'simpleImageQuery', $request );
45 },
46 ) );
47 register_rest_route( 'mwai/v1', '/simpleVisionQuery', array(
48 'methods' => 'POST',
49 'callback' => array( $this, 'rest_simpleVisionQuery' ),
50 'permission_callback' => function( $request ) {
51 return $this->core->can_access_public_api( 'simpleVisionQuery', $request );
52 },
53 ) );
54 register_rest_route( 'mwai/v1', '/simpleJsonQuery', array(
55 'methods' => 'POST',
56 'callback' => array( $this, 'rest_simpleJsonQuery' ),
57 'permission_callback' => function( $request ) {
58 return $this->core->can_access_public_api( 'simpleJsonQuery', $request );
59 },
60 ) );
61 register_rest_route( 'mwai/v1', '/moderationCheck', array(
62 'methods' => 'POST',
63 'callback' => array( $this, 'rest_moderationCheck' ),
64 'permission_callback' => function( $request ) {
65 return $this->core->can_access_public_api( 'moderationCheck', $request );
66 },
67 ) );
68
69 if ( $this->chatbot_module ) {
70 register_rest_route( 'mwai/v1', '/simpleChatbotQuery', array(
71 'methods' => 'POST',
72 'callback' => array( $this, 'rest_simpleChatbotQuery' ),
73 'permission_callback' => function( $request ) {
74 return $this->core->can_access_public_api( 'simpleChatbotQuery', $request );
75 },
76 ) );
77 }
78 }
79
80 public function rest_simpleAuthCheck( $request ) {
81 try {
82 $params = $request->get_params();
83 $current_user = wp_get_current_user();
84 $current_email = $current_user->user_email;
85 return new WP_REST_Response([ 'success' => true, 'data' => [
86 'type' => 'email',
87 'value' => $current_email
88 ] ], 200 );
89 }
90 catch (Exception $e) {
91 return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
92 }
93 }
94
95 public function auth_via_bearer_token( $allow, $feature, $extra ) {
96 if ( !empty( $extra ) && !empty( $extra->get_header( 'Authorization' ) ) ) {
97 $token = $extra->get_header( 'Authorization' );
98 $token = str_replace( 'Bearer ', '', $token );
99 if ( $token === $this->bearer_token ) {
100 // We set the current user to the first admin.
101 $admin = $this->core->get_admin_user();
102 wp_set_current_user( $admin->ID, $admin->user_login );
103 return true;
104 }
105 }
106 return $allow;
107 }
108
109 public function rest_simpleChatbotQuery( $request ) {
110 try {
111 $params = $request->get_params();
112 $botId = isset( $params['botId'] ) ? $params['botId'] : '';
113 $message = isset( $params['message'] ) ? $params['message'] : '';
114 if ( empty( $message ) ) {
115 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
116 }
117 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : null;
118 $params = null;
119 if ( !empty( $chatId ) ) {
120 $params = array( 'chatId' => $chatId );
121 }
122 if ( empty( $botId ) || empty( $message ) ) {
123 throw new Exception( 'The botId and message are required.' );
124 }
125 $reply = $this->simpleChatbotQuery( $botId, $message, $params, false );
126 return new WP_REST_Response([
127 'success' => true,
128 'data' => $reply['reply'],
129 'extra' => [
130 'actions' => $reply['actions'],
131 'chatId' => $reply['chatId']
132 ]
133 ], 200 );
134 }
135 catch (Exception $e) {
136 return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
137 }
138 }
139
140
141 public function rest_simpleTextQuery( $request ) {
142 try {
143 $params = $request->get_params();
144 $message = isset( $params['message'] ) ? $params['message'] : '';
145 if ( empty( $message ) ) {
146 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
147 }
148 $options = isset( $params['options'] ) ? $params['options'] : [];
149 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
150 if ( !empty( $scope ) ) {
151 $options['scope'] = $scope;
152 }
153 if ( empty( $message ) ) {
154 throw new Exception( 'The message is required.' );
155 }
156 $reply = $this->simpleTextQuery( $message, $options );
157 return new WP_REST_Response([ 'success' => true, 'data' => $reply ], 200 );
158 }
159 catch (Exception $e) {
160 return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
161 }
162 }
163
164 public function rest_simpleImageQuery( $request ) {
165 try {
166 $params = $request->get_params();
167 $message = isset( $params['message'] ) ? $params['message'] : '';
168 if ( empty( $message ) ) {
169 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
170 }
171 $options = isset( $params['options'] ) ? $params['options'] : [];
172 $resolution = isset( $params['resolution'] ) ? $params['resolution'] : '';
173 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
174 if ( !empty( $scope ) ) {
175 $options['scope'] = $scope;
176 }
177 if ( empty( $message ) ) {
178 throw new Exception( 'The message is required.' );
179 }
180 if ( !empty( $resolution ) ) {
181 $options['resolution'] = $resolution;
182 }
183 $reply = $this->simpleImageQuery( $message, $options );
184 return new WP_REST_Response([ 'success' => true, 'data' => $reply ], 200 );
185 }
186 catch (Exception $e) {
187 return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
188 }
189 }
190
191 public function rest_simpleVisionQuery( $request ) {
192 try {
193 $params = $request->get_params();
194 $message = isset( $params['message'] ) ? $params['message'] : '';
195 if ( empty( $message ) ) {
196 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
197 }
198 $url = isset( $params['url'] ) ? $params['url'] : '';
199 $options = isset( $params['options'] ) ? $params['options'] : [];
200 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
201 if ( !empty( $scope ) ) {
202 $options['scope'] = $scope;
203 }
204 if ( empty( $message ) ) {
205 throw new Exception( 'The message is required.' );
206 }
207 if ( empty( $url ) ) {
208 throw new Exception( 'The url is required.' );
209 }
210 $reply = $this->simpleVisionQuery( $message, $url, null, $options );
211 return new WP_REST_Response([ 'success' => true, 'data' => $reply ], 200 );
212 }
213 catch (Exception $e) {
214 return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
215 }
216 }
217
218 public function rest_simpleJsonQuery( $request ) {
219 try {
220 $params = $request->get_params();
221 $message = isset( $params['message'] ) ? $params['message'] : '';
222 if ( empty( $message ) ) {
223 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
224 }
225 $options = isset( $params['options'] ) ? $params['options'] : [];
226 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
227 if ( !empty( $scope ) ) {
228 $options['scope'] = $scope;
229 }
230 if ( empty( $message ) ) {
231 throw new Exception( 'The message is required.' );
232 }
233 $reply = $this->simpleJsonQuery( $message, $options );
234 return new WP_REST_Response([ 'success' => true, 'data' => $reply ], 200 );
235 }
236 catch (Exception $e) {
237 return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
238 }
239 }
240
241 public function rest_moderationCheck( $request ) {
242 try {
243 $params = $request->get_params();
244 $text = $params['text'];
245 $reply = $this->moderationCheck( $text );
246 return new WP_REST_Response([ 'success' => true, 'data' => $reply ], 200 );
247 }
248 catch (Exception $e) {
249 return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
250 }
251 }
252 #endregion
253
254 #region Simple API
255 /**
256 * Executes a vision query.`
257 *
258 * @param string $message The prompt for the AI.
259 * @param string $url The URL of the image to analyze.
260 * @param string|null $path The path to the image file. If provided, the image data will be read from this file.
261 * @param array $params Additional parameters for the AI query.
262 *
263 * @return string The result of the AI query.
264 */
265 public function simpleVisionQuery( $message, $url, $path = null, $params = [] ) {
266 global $mwai_core;
267 $ai_vision_default_env = $this->core->get_option( 'ai_vision_default_env' );
268 $ai_vision_default_model = $this->core->get_option( 'ai_vision_default_model' );
269 if ( empty( $ai_vision_default_model ) ) {
270 $ai_vision_default_model = MWAI_FALLBACK_MODEL_VISION;
271 }
272 $query = new Meow_MWAI_Query_Text( $message );
273 if ( !empty( $ai_vision_default_env ) ) {
274 $query->set_env_id( $ai_vision_default_env );
275 }
276 if ( !empty( $ai_vision_default_model ) ) {
277 $query->set_model( $ai_vision_default_model );
278 }
279 $query->inject_params( $params );
280 if ( !empty( $url ) ) {
281 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision' ) );
282 }
283 else if ( !empty( $path ) ) {
284 $query->set_file( Meow_MWAI_Query_DroppedFile::from_path( $path, 'vision' ) );
285 }
286 $reply = $mwai_core->run_query( $query );
287 return $reply->result;
288 }
289
290 /**
291 * Executes a chatbot query.
292 * It will use the discussion if chatId is provided in the parameters.
293 *
294 * @param string $botId The ID of the chatbot.
295 * @param string $message The prompt for the AI.
296 * @param array $params Additional parameters for the AI query.
297 *
298 * @return string The result of the AI query.
299 */
300 public function simpleChatbotQuery( $botId, $message, $params = [], $onlyReply = true ) {
301 if ( !isset( $params['messages'] ) && isset( $params['chatId'] ) ) {
302 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
303 $discussion = $this->discussions_module->get_discussion( $botId, $params['chatId'] );
304 if ( !empty( $discussion ) ) {
305 $params['messages'] = $discussion['messages'];
306 }
307 }
308 else {
309 $this->core->log( 'The chatId was provided; but the discussions are not enabled.' );
310 }
311 }
312 $data = $this->chatbot_module->chat_submit( $botId, $message, null, $params );
313 return $onlyReply ? $data['reply'] : $data;
314 }
315
316 /**
317 * Executes a text query.
318 *
319 * @param string $message The prompt for the AI.
320 * @param array $params Additional parameters for the AI query.
321 *
322 * @return string The result of the AI query.
323 */
324 public function simpleTextQuery( $message, $params = [] ) {
325 global $mwai_core;
326 $query = new Meow_MWAI_Query_Text( $message );
327 $query->inject_params( $params );
328 $reply = $mwai_core->run_query( $query );
329 return $reply->result;
330 }
331
332 public function simpleImageQuery( $message, $params = [] ) {
333 global $mwai_core;
334 $query = new Meow_MWAI_Query_Image( $message );
335 $query->inject_params( $params );
336 $reply = $mwai_core->run_query( $query );
337 return $reply->result;
338 }
339
340 /**
341 * Generates an image relevant to the text.
342 */
343 public function imageQueryForMediaLibrary( $message, $params = [], $postId = null ) {
344 $query = new Meow_MWAI_Query_Image( $message );
345 $query->inject_params( $params );
346 $query->set_local_download( null );
347 $reply = $this->core->run_query( $query );
348 preg_match( '/\!\[Image\]\((.*?)\)/', $reply->result, $matches );
349 $url = $matches[1] ?? $reply->result;
350 $attachmentId = $this->core->add_image_from_url( $url, null, null, null, null, null, $postId );
351 if ( empty( $attachmentId ) ) {
352 throw new Exception( 'Could not add the image to the Media Library.' );
353 }
354 // TODO: We should create a nice title, caption, and alt.
355 $media = [
356 'id' => $attachmentId,
357 'url' => wp_get_attachment_url( $attachmentId ),
358 'title' => get_the_title( $attachmentId ),
359 'caption' => wp_get_attachment_caption( $attachmentId ),
360 'alt' => get_post_meta( $attachmentId, '_wp_attachment_image_alt', true )
361 ];
362 return $media;
363 }
364
365 /**
366 * Executes a query that will have to return a JSON result.
367 *
368 * @param string $message The prompt for the AI.
369 * @param array $params Additional parameters for the AI query.
370 *
371 * @return array The result of the AI query.
372 */
373 public function simpleJsonQuery( $message, $url = null, $path = null, $params = [] ) {
374 if ( !empty( $url ) || !empty( $path ) ) {
375 throw new Exception( 'The url and path are not supported yet by the simpleJsonQuery.' );
376 }
377 global $mwai_core;
378 $query = new Meow_MWAI_Query_Text( $message . "\nYour reply must be a formatted JSON." );
379 $query->inject_params( $params );
380 $query->set_response_format( 'json' );
381 $ai_json_default_env = $mwai_core->get_option( 'ai_json_default_env' );
382 $ai_json_default_model = $mwai_core->get_option( 'ai_json_default_model' );
383 if ( !empty( $ai_json_default_env ) ) {
384 $query->set_env_id( $ai_json_default_env );
385 }
386 if ( !empty( $ai_json_default_model ) ) {
387 $query->set_model( $ai_json_default_model );
388 }
389 else {
390 $query->set_model( MWAI_FALLBACK_MODEL_JSON );
391 }
392 $reply = $mwai_core->run_query( $query );
393 try {
394 $json = json_decode( $reply->result, true );
395 return $json;
396 }
397 catch ( Exception $e ) {
398 throw new Exception( 'The result is not a valid JSON.' );
399 }
400 }
401 #endregion
402
403 #region Standard API
404 /**
405 * Checks if a text is safe or not.
406 *
407 * @param string $text The text to check.
408 *
409 * @return bool True if the text is safe, false otherwise.
410 */
411 public function moderationCheck( $text ) {
412 global $mwai_core;
413 $openai = Meow_MWAI_Engines_Factory::get_openai( $mwai_core );
414 $res = $openai->moderate( $text );
415 if ( !empty( $res ) && !empty( $res['results'] ) ) {
416 return (bool)$res['results'][0]['flagged'];
417 }
418 }
419 #endregion
420
421 #region Standard API (No REST API)
422
423 /**
424 * Checks the status of the AI environments.
425 *
426 * @return array The types of environments that are available.
427 */
428 public function checkStatus() {
429 $env_types = [];
430 $ai_envs = $this->core->get_option( 'ai_envs' );
431 if ( empty( $ai_envs ) ) {
432 throw new Exception( 'There are no AI environments yet.' );
433 }
434 foreach ( $ai_envs as $env ) {
435 if ( !empty( $env['apikey'] ) ) {
436 if ( !in_array( $env['type'], $env_types ) ) {
437 $env_types[] = $env['type'];
438 }
439 }
440 }
441 if ( empty( $env_types ) ) {
442 throw new Exception( 'There are no AI environments with an API key yet.' );
443 }
444 return $env_types;
445 }
446 #endregion
447 }