PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.3.1
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.3.1
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 / core.php
ai-engine / classes Last commit date
engines 2 years ago modules 2 years ago queries 2 years ago admin.php 2 years ago api.php 2 years ago core.php 2 years ago init.php 3 years ago reply.php 2 years ago rest.php 2 years ago
core.php
1111 lines
1 <?php
2
3 require_once( MWAI_PATH . '/vendor/autoload.php' );
4 require_once( MWAI_PATH . '/constants/init.php' );
5
6 define( 'MWAI_IMG_WAND', MWAI_URL . '/images/wand.png' );
7 define( 'MWAI_IMG_WAND_HTML', "<img style='height: 22px; margin-bottom: -5px; margin-right: 8px;'
8 src='" . MWAI_IMG_WAND . "' alt='AI Wand' />" );
9 define( 'MWAI_IMG_WAND_HTML_XS', "<img style='height: 16px; margin-bottom: -2px;'
10 src='" . MWAI_IMG_WAND . "' alt='AI Wand' />" );
11
12 class Meow_MWAI_Core
13 {
14 public $admin = null;
15 public $is_rest = false;
16 public $is_cli = false;
17 public $site_url = null;
18 public $files = null;
19 public $tasks = null;
20 public $magicWand = null;
21 private $option_name = 'mwai_options';
22 private $themes_option_name = 'mwai_themes';
23 private $chatbots_option_name = 'mwai_chatbots';
24 private $nonce = null;
25
26 public $chatbot = null;
27 public $discussions = null;
28
29 public function __construct() {
30 $this->site_url = get_site_url();
31 $this->is_rest = MeowCommon_Helpers::is_rest();
32 $this->is_cli = defined( 'WP_CLI' );
33 $this->files = new Meow_MWAI_Modules_Files( $this );
34 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
35
36 if ( $this->get_option( 'module_suggestions' ) ) {
37 $this->magicWand = new Meow_MWAI_Modules_Wand( $this );
38 }
39
40 add_action( 'plugins_loaded', array( $this, 'init' ) );
41 add_action( 'wp_register_script', array( $this, 'register_scripts' ) );
42 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
43 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
44 }
45
46 #region Init & Scripts
47 function init() {
48 global $mwai;
49 $this->chatbot = null;
50 $this->discussions = null;
51 new Meow_MWAI_Modules_Security( $this );
52 if ( $this->is_rest ) {
53 new Meow_MWAI_Rest( $this );
54 }
55 if ( is_admin() ) {
56 new Meow_MWAI_Admin( $this );
57 new Meow_MWAI_Modules_Utilities( $this );
58 }
59 if ( $this->get_option( 'shortcode_chat' ) ) {
60 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
61 $this->discussions = new Meow_MWAI_Modules_Discussions();
62 }
63
64 // Advanced core
65 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
66 new MeowPro_MWAI_Core( $this );
67 }
68
69 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
70 }
71
72 public function register_scripts() {
73 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
74 }
75
76 public function enqueue_scripts() {
77 $this->register_scripts();
78 wp_enqueue_script( "mwai_highlight" );
79 }
80
81 #endregion
82
83 #region Roles & Capabilities
84
85 function can_access_settings() {
86 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
87 }
88
89 function can_access_features() {
90 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
91 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
92 }
93
94 function can_access_public_api( $feature, $extra ) {
95 $logged_in = is_user_logged_in();
96 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
97 }
98
99 #endregion
100
101 #region AI-Related Helpers
102 function run_query( $query, $streamCallback = null, $markdown = false ) {
103 $envId = !empty( $query->envId ) ? $query->envId : null;
104 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
105
106 // If the engine is not set, we need to set it to the default one.
107 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
108 if ( $query instanceof Meow_MWAI_Query_Text ) {
109 $this->set_if_empty_defaults( $query, 'ai_default_env', 'ai_default_model' );
110 }
111 if ( $query instanceof Meow_MWAI_Query_Embed ) {
112 $this->set_if_empty_defaults( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
113 }
114 else if ( $query instanceof Meow_MWAI_Query_Image ) {
115 $this->set_if_empty_defaults( $query, 'ai_images_default_env', 'ai_images_default_model' );
116 }
117 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
118 $this->set_if_empty_defaults( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
119 }
120 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
121 }
122
123 // Let's run the query.
124 $reply = $engine->run( $query, $streamCallback );
125
126 // Let's allow to modify the reply before it is sent.
127 if ( $markdown ) {
128 if ( $query instanceof Meow_MWAI_Query_Image ) {
129 $reply->result = "";
130 foreach ( $reply->results as $result ) {
131 $reply->result .= "![Image]($result)\n";
132 }
133 }
134 }
135
136 return $reply;
137 }
138
139 private function set_if_empty_defaults( $query, $envOption, $modelOption ) {
140 $defaultEnv = $this->get_option( $envOption );
141 $defaultModel = $this->get_option( $modelOption );
142 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
143 throw new Exception( 'AI Engine: The default environment and model are not set.' );
144 }
145 $query->set_env_id( $defaultEnv );
146 $query->set_model( $defaultModel );
147 }
148 #endregion
149
150 #region Text-Related Helpers
151
152 // Clean the text perfectly, resolve shortcodes, etc, etc.
153 function clean_text( $rawText = "" ) {
154 $text = html_entity_decode( $rawText );
155 $text = wp_strip_all_tags( $text );
156 $text = preg_replace( '/[\r\n]+/', "\n", $text );
157 $text = preg_replace( '/\n+/', "\n", $text );
158 $text = preg_replace( '/\t+/', "\t", $text );
159 return $text . " ";
160 }
161
162 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
163 function clean_sentences( $text, $maxLength = null ) {
164 // Step 1: Identify URLs and replace them with a placeholder.
165 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
166 preg_match_all($urlPattern, $text, $urls);
167 $urlPlaceholders = array();
168 foreach ($urls[0] as $index => $url) {
169 $placeholder = "{urlPlaceholder" . $index . "}";
170 $text = str_replace($url, $placeholder, $text);
171 $urlPlaceholders[$placeholder] = $url;
172 }
173
174 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
175 $sentences = preg_split('/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
176 $hashes = array();
177 $uniqueSentences = array();
178 $total = 0;
179
180 foreach ( $sentences as $sentence ) {
181 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
182 $hash = md5( $sentence );
183 if ( !in_array( $hash, $hashes ) ) {
184 $length = mb_strlen( $sentence, 'UTF-8' );
185 if ( $total + $length > $maxLength ) {
186 continue;
187 }
188 $hashes[] = $hash;
189 $uniqueSentences[] = $sentence;
190 $total += $length;
191 }
192 }
193
194 $freshText = implode( " ", $uniqueSentences );
195
196 // Step 3: Restore URLs in the final text.
197 foreach ($urlPlaceholders as $placeholder => $url) {
198 $freshText = str_replace($placeholder, $url, $freshText);
199 }
200
201 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
202 return $freshText;
203 }
204
205 function get_post_content( $postId ) {
206 $post = get_post( $postId );
207 if ( !$post ) {
208 return false;
209 }
210 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
211 $pattern = '/\[mwai_.*?\]/';
212 $text = preg_replace( $pattern, '', $text );
213 if ( $this->get_option( 'resolve_shortcodes' ) ) {
214 $text = apply_filters( 'the_content', $text );
215 }
216 else {
217 $pattern = "/\[[^\]]+\]/";
218 $text = preg_replace( $pattern, '', $text );
219 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
220 $text = preg_replace( $pattern, '', $text );
221 }
222 $text = $this->clean_text( $text );
223 $text = $this->clean_sentences( $text );
224 $text = apply_filters( 'mwai_post_content', $text, $postId );
225 return $text;
226 }
227
228 function markdown_to_html( $content ) {
229 $Parsedown = new Parsedown();
230 $content = $Parsedown->text( $content );
231 return $content;
232 }
233
234 function get_post_language( $postId ) {
235 $locale = get_locale();
236 $code = strtolower( substr( $locale, 0, 2 ) );
237 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
238 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
239 if ( !empty( $lang ) ) {
240 $locale = $lang['locale'];
241 $humanLanguage = $lang['display_name'];
242 }
243 return strtolower( "$locale ($humanLanguage)" );
244 }
245 #endregion
246
247 #region Image-Related Helpers
248 static function get_mime_type( $file ) {
249 $mimeType = null;
250
251 // Let's try to use mime_content_type if the function exists
252 if ( function_exists( 'mime_content_type' ) ) {
253 $mimeType = mime_content_type( $file );
254 }
255
256 // Otherwise, let's check the file extension (which can actually also be an URL)
257 if ( !$mimeType ) {
258 $extension = pathinfo( $file, PATHINFO_EXTENSION );
259 $extension = strtolower( $extension );
260 $mimeTypes = [
261 'jpg' => 'image/jpeg',
262 'jpeg' => 'image/jpeg',
263 'png' => 'image/png',
264 'gif' => 'image/gif',
265 'webp' => 'image/webp',
266 'bmp' => 'image/bmp',
267 'tiff' => 'image/tiff',
268 'tif' => 'image/tiff',
269 'svg' => 'image/svg+xml',
270 'ico' => 'image/x-icon',
271 'pdf' => 'application/pdf',
272 ];
273 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
274 }
275
276 return $mimeType;
277 }
278
279 function download_image( $url ) {
280 $args = array( 'timeout' => 60, );
281 $response = wp_safe_remote_get( $url, $args );
282 if ( is_wp_error( $response ) ) {
283 throw new Exception( $response->get_error_message() );
284 }
285 $output = wp_remote_retrieve_body( $response );
286 if ( is_wp_error( $output ) ) {
287 throw new Exception( $output->get_error_message() );
288 }
289 return $output;
290 }
291
292 /**
293 * Add an image from a URL to the Media Library.
294 * @param string $url The URL of the image to be downloaded.
295 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
296 * @param string $title The title of the image.
297 * @param string $description The description of the image.
298 * @param string $caption The caption of the image.
299 * @param string $alt The alt text of the image.
300 * @return int The attachment ID of the image.
301 */
302 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null ) {
303 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
304 $url_filename = $path_parts['basename'];
305 $file_type = wp_check_filetype( $url_filename, null );
306 $allowed_types = get_allowed_mime_types();
307 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
308 throw new Exception( 'Invalid file type from URL.' );
309 }
310
311 // Initial extension from URL file name
312 $extension = $file_type['ext'];
313
314 if ( !empty( $filename ) ) {
315 $custom_file_type = wp_check_filetype( $filename, null );
316 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
317 throw new Exception( 'Invalid custom file type.' );
318 }
319 // Use the extension from the custom filename if valid
320 $extension = $custom_file_type['ext'];
321 }
322
323 $image_data = $this->download_image( $url );
324 if ( !$image_data ) {
325 throw new Exception( 'Could not download the image.' );
326 }
327 $upload_dir = wp_upload_dir();
328
329 // Filename handling including 'generated_' prefix scenario
330 if ( empty( $filename ) ) {
331 $filename = sanitize_file_name( $url_filename );
332 if ( empty( $extension ) ) { // This condition might now be redundant
333 $extension = $file_type['ext'];
334 }
335 // Filename length check and prepend if conditions met
336 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
337 $filename = $this->get_random_id( 16 ) . '.' . $extension;
338 }
339 if ( strpos( $filename, '.' ) === false ) {
340 $filename .= '.' . $extension;
341 }
342 }
343
344 // Directory and file path handling
345 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
346 $file = $upload_dir['path'] . '/' . $filename;
347 }
348 else {
349 $file = $upload_dir['basedir'] . '/' . $filename;
350 }
351
352 // Ensure file name uniqueness in the directory
353 $i = 1;
354 $parts = pathinfo( $file );
355 while ( file_exists( $file ) ) {
356 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
357 $i++;
358 }
359
360 // Writing the file to disk
361 file_put_contents( $file, $image_data );
362
363 // Attachment and metadata handling in WP
364 $attachment = [
365 'post_mime_type' => $file_type['type'],
366 'post_title' => $title ?? '',
367 'post_content' => $description ?? '',
368 'post_excerpt' => $caption ?? '',
369 'post_status' => 'inherit'
370 ];
371 $attachmentId = wp_insert_attachment( $attachment, $file );
372 require_once( ABSPATH . 'wp-admin/includes/image.php' );
373 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
374 wp_update_attachment_metadata( $attachmentId, $attachment_data );
375 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
376
377 return $attachmentId;
378 }
379 #endregion
380
381 #region Context-Related Helpers
382 function retrieve_context( $params, $query ) {
383 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
384 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
385 $context = apply_filters( 'mwai_context_search', [], $query, [
386 'embeddingsEnvId' => $embeddingsEnvId
387 ]);
388 if ( empty( $context ) ) {
389 return null;
390 }
391 else if ( !isset( $context['content'] ) ) {
392 error_log( "AI Engine: A context without content was returned." );
393 return null;
394 }
395 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
396 $context['length'] = strlen( $context['content'] );
397 return $context;
398 }
399 #endregion
400
401 #region Users/Sessions Helpers
402
403 function get_nonce() {
404 // if ( !is_user_logged_in() ) {
405 // return null;
406 // }
407 if ( isset( $this->nonce ) ) {
408 return $this->nonce;
409 }
410 $this->nonce = wp_create_nonce( 'wp_rest' );
411 return $this->nonce;
412 }
413
414 function get_session_id() {
415 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
416 return $_COOKIE['mwai_session_id'];
417 }
418 return "N/A";
419 }
420
421 // Get the UserID from the data, or from the current user
422 function get_user_id( $data = null ) {
423 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
424 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
425 $admin = get_users( [ 'role' => 'administrator' ] );
426 if ( !empty( $admin ) ) {
427 return $admin[0]->ID;
428 }
429 }
430 if ( isset( $data ) && isset( $data['userId'] ) ) {
431 return (int)$data['userId'];
432 }
433 if ( is_user_logged_in() ) {
434 $current_user = wp_get_current_user();
435 if ( $current_user->ID > 0 ) {
436 return $current_user->ID;
437 }
438 }
439 return null;
440 }
441
442 function get_admin_user() {
443 $admin = get_users( [ 'role' => 'administrator' ] );
444 if ( !empty( $admin ) ) {
445 return $admin[0];
446 }
447 return null;
448 }
449
450 function get_user_data() {
451 $user = wp_get_current_user();
452 if ( empty( $user ) || empty( $user->ID ) ) {
453 return null;
454 }
455 $placeholders = array(
456 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
457 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
458 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
459 $user->data->user_login : null,
460 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
461 $user->data->display_name : null,
462 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
463 );
464 return $placeholders;
465 }
466
467 function get_ip_address( $params = null ) {
468 $ip = '127.0.0.1';
469 $headers = [
470 'HTTP_TRUE_CLIENT_IP',
471 'HTTP_CF_CONNECTING_IP',
472 'HTTP_X_REAL_IP',
473 'HTTP_CLIENT_IP',
474 'HTTP_X_FORWARDED_FOR',
475 'HTTP_X_FORWARDED',
476 'HTTP_X_CLUSTER_CLIENT_IP',
477 'HTTP_FORWARDED_FOR',
478 'HTTP_FORWARDED',
479 'REMOTE_ADDR',
480 ];
481
482 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
483 $ip = ( string )$params[ 'ip' ];
484 } else {
485 foreach ( $headers as $header ) {
486 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
487 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
488 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
489 break;
490 }
491 }
492 }
493
494 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
495 }
496
497 #endregion
498
499 #region Other Helpers
500
501 public function check_rest_nonce( $request ) {
502 $nonce = $request->get_header( 'X-WP-Nonce' );
503 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
504 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
505 }
506
507 function get_random_id( $length = 8, $excludeIds = [] ) {
508 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
509 $charactersLength = strlen( $characters );
510 $randomId = '';
511 for ( $i = 0; $i < $length; $i++ ) {
512 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
513 }
514 if ( in_array( $randomId, $excludeIds ) ) {
515 return $this->get_random_id( $length, $excludeIds );
516 }
517 return $randomId;
518 }
519
520 function is_url( $url ) {
521 return strpos( $url, 'http' ) === 0 ? true : false;
522 }
523
524 function get_post_types() {
525 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
526 $post_types = array();
527 $types = get_post_types( [], 'objects' );
528
529 // Let's get the Post Types that are enabled for Embeddings Sync
530 $embeddingsSettings = $this->get_option( 'embeddings' );
531 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
532
533 foreach ( $types as $type ) {
534 $forced = in_array( $type->name, $syncPostTypes );
535 // Should not be excluded.
536 if ( !$forced && in_array( $type->name, $excluded ) ) {
537 continue;
538 }
539 // Should be public.
540 if ( !$forced && !$type->public ) {
541 continue;
542 }
543 $post_types[] = array(
544 'name' => $type->labels->name,
545 'type' => $type->name,
546 );
547 }
548
549 // Let's get the Post Types that are enabled for Embeddings Sync
550 $embeddingsSettings = $this->get_option( 'embeddings' );
551 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
552
553 return $post_types;
554 }
555
556 function get_post( $post ) {
557 if ( is_numeric( $post ) ) {
558 $post = get_post( $post );
559 }
560 if ( is_object( $post ) ) {
561 $post = (array)$post;
562 }
563 if ( !is_array( $post ) ) {
564 return null;
565 }
566 $language = $this->get_post_language( $post['ID'] );
567 $content = $this->get_post_content( $post['ID'] );
568 $title = $post['post_title'];
569 $excerpt = $post['post_excerpt'];
570 $url = get_permalink( $post['ID'] );
571 $checksum = wp_hash( $content . $title . $url );
572 return [
573 'postId' => (int)$post['ID'],
574 'title' => $title,
575 'content' => $content,
576 'excerpt' => $excerpt,
577 'url' => $url,
578 'language' => $language ?? 'english',
579 'checksum' => $checksum,
580 ];
581 }
582 #endregion
583
584 #region Usage & Costs
585
586 // Quick and dirty token estimation
587 // Let's keep this synchronized with Helpers in JS
588 static function estimate_tokens( ...$args ): int {
589 $text = "";
590 foreach ( $args as $arg ) {
591 if ( is_array( $arg ) ) {
592 foreach ( $arg as $message ) {
593 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
594 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
595 }
596 }
597 else if ( is_string( $arg ) ) {
598 $text .= $arg;
599 }
600 }
601 $averageTokenLength = 4;
602 $words = preg_split( '/\s+/', trim( $text ) );
603 $tokenCount = 0;
604 foreach ( $words as $word ) {
605 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
606 }
607 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
608 }
609
610 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
611 if ( !is_numeric( $in_tokens ) ) {
612 throw new Exception( 'AI Engine: in_tokens must be a number.' );
613 }
614 if ( !is_numeric( $out_tokens ) ) {
615 $out_tokens = 0;
616 }
617 if ( !$model ) {
618 throw new Exception( 'AI Engine: model is required.' );
619 }
620 $usage = $this->get_option( 'openai_usage' );
621 $month = date( 'Y-m' );
622 if ( !isset( $usage[$month] ) ) {
623 $usage[$month] = array();
624 }
625 if ( !isset( $usage[$month][$model] ) ) {
626 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
627 }
628 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
629 $usage[$month][$model]['completion_tokens'] += $out_tokens;
630 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
631 $this->update_option( 'openai_usage', $usage );
632 $usageInfo = [
633 'prompt_tokens' => $in_tokens,
634 'completion_tokens' => $out_tokens,
635 'total_tokens' => $in_tokens + $out_tokens,
636 ];
637 if ( $returned_price !== null ) {
638 $usageInfo['price'] = $returned_price;
639 }
640 return $usageInfo;
641 }
642
643 public function record_audio_usage( $model, $seconds ) {
644 if ( !is_numeric( $seconds ) ) {
645 throw new Exception( 'AI Engine: seconds must be a number.' );
646 }
647 if ( !$model ) {
648 throw new Exception( 'AI Engine: model is required.' );
649 }
650 $usage = $this->get_option( 'openai_usage' );
651 $month = date( 'Y-m' );
652 if ( !isset( $usage[$month] ) ) {
653 $usage[$month] = array();
654 }
655 if ( !isset( $usage[$month][$model] ) ) {
656 $usage[$month][$model] = array( 'seconds' => 0 );
657 }
658 $usage[$month][$model]['seconds'] += $seconds;
659 $this->update_option( 'openai_usage', $usage );
660 return [ 'seconds' => $seconds ];
661 }
662
663 public function record_images_usage( $model, $resolution, $images ) {
664 if ( !$model || !$resolution || !$images ) {
665 throw new Exception( 'Missing parameters for record_image_usage.' );
666 }
667 $usage = $this->get_option( 'openai_usage' );
668 $month = date( 'Y-m' );
669 if ( !isset( $usage[$month] ) ) {
670 $usage[$month] = array();
671 }
672 if ( !isset( $usage[$month][$model] ) ) {
673 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
674 }
675 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
676 $usage[$month][$model]['resolution'][$resolution] = 0;
677 }
678 $usage[$month][$model]['resolution'][$resolution] += $images;
679 $usage[$month][$model]['images'] += $images;
680 $this->update_option( 'openai_usage', $usage );
681 return [ 'resolution' => $resolution, 'images' => $images ];
682 }
683
684 #endregion
685
686 #region Streaming
687 public function stream_push( $data ) {
688 $out = "data: " . json_encode( $data );
689 echo $out;
690 echo "\n\n";
691 if ( ob_get_level() > 0 ) {
692 ob_end_flush();
693 }
694 flush();
695 }
696 #endregion
697
698 #region Options
699 function get_themes() {
700 $themes = get_option( $this->themes_option_name, [] );
701 $themes = empty( $themes ) ? [] : $themes;
702
703 $internalThemes = [
704 'chatgpt' => [
705 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
706 'settings' => [], 'style' => ""
707 ],
708 'messages' => [
709 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
710 'settings' => [], 'style' => ""
711 ],
712 ];
713 $customThemes = [];
714 foreach ( $themes as $theme ) {
715 if ( isset( $internalThemes[$theme['themeId']] ) ) {
716 $internalThemes[$theme['themeId']] = $theme;
717 continue;
718 }
719 $customThemes[] = $theme;
720 }
721 return array_merge(array_values($internalThemes), $customThemes);
722 }
723
724 function update_themes( $themes ) {
725 update_option( $this->themes_option_name, $themes );
726 return $themes;
727 }
728
729 function get_chatbots() {
730 $chatbots = get_option( $this->chatbots_option_name, [] );
731 $hasChanges = false;
732 if ( empty( $chatbots ) ) {
733 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
734 }
735 $hasDefault = false;
736 foreach ( $chatbots as &$chatbot ) {
737 if ( $chatbot['botId'] === 'default' ) {
738 $hasDefault = true;
739 }
740 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
741 // Use default value if not set.
742 if ( !isset( $chatbot[$key] ) ) {
743 $chatbot[$key] = $value;
744 }
745 }
746 // TODO: After October 2024, let's remove this.
747 if ( isset( $chatbot['context'] ) ) {
748 $chatbot['instructions'] = $chatbot['context'];
749 unset( $chatbot['context'] );
750 $hasChanges = true;
751 }
752 // TODO: After October 2024, let's remove this.
753 if ( isset( $chatbot['fileUpload'] ) ) {
754 $chatbot['fileSearch'] = $chatbot['fileUpload'];
755 unset( $chatbot['fileUpload'] );
756 $hasChanges = true;
757 }
758 }
759 if ( !$hasDefault ) {
760 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
761 array_unshift( $chatbots, $defaultBot );
762 $hasChanges = true;
763 }
764 if ( $hasChanges ) {
765 update_option( $this->chatbots_option_name, $chatbots );
766 }
767 return $chatbots;
768 }
769
770 function get_chatbot( $botId ) {
771 $chatbots = $this->get_chatbots();
772 foreach ( $chatbots as $chatbot ) {
773 if ( $chatbot['botId'] === (string)$botId ) {
774 return $chatbot;
775 }
776 }
777 return null;
778 }
779
780 function get_embeddings_env( $envId ) {
781 $envs = $this->get_option( 'embeddings_envs' );
782 foreach ( $envs as $env ) {
783 if ( $env['id'] === $envId ) {
784 return $env;
785 }
786 }
787 return null;
788 }
789
790 function get_ai_env( $envId ) {
791 $envs = $this->get_option( 'ai_envs' );
792 foreach ( $envs as $env ) {
793 if ( $env['id'] === $envId ) {
794 return $env;
795 }
796 }
797 return null;
798 }
799
800 function get_assistant( $envId, $assistantId ) {
801 $env = $this->get_ai_env( $envId );
802 if ( !$env ) {
803 return null;
804 }
805 $assistants = $env['assistants'];
806 foreach ( $assistants as $assistant ) {
807 if ( $assistant['id'] === $assistantId ) {
808 return $assistant;
809 }
810 }
811 return null;
812 }
813
814 function get_theme( $themeId ) {
815 $themes = $this->get_themes();
816 foreach ( $themes as $theme ) {
817 if ( $theme['themeId'] === $themeId ) {
818 return $theme;
819 }
820 }
821 return null;
822 }
823
824 function update_chatbots( $chatbots ) {
825 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
826 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
827 $keepLineReturnsFields = [ 'instructions' ];
828 $whiteSpacedFields = [ 'context' ];
829 foreach ( $chatbots as &$chatbot ) {
830 foreach ( $chatbot as $key => &$value ) {
831 if ( in_array( $key, $deprecatedFields ) ) {
832 unset( $chatbot[$key] );
833 continue;
834 }
835 if ( in_array( $key, $htmlFields ) ) {
836 $value = wp_kses_post( $value );
837 }
838 else if ( in_array( $key, $whiteSpacedFields ) ) {
839 $value = sanitize_textarea_field( $value );
840 }
841 else if ( $key === 'functions' ) {
842 $functions = [];
843 foreach ( $value as $function ) {
844 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
845 $functions[] = [
846 'id' => sanitize_text_field( $function['id'] ),
847 'type' => sanitize_text_field( $function['type'] ),
848 ];
849 }
850 }
851 $value = $functions;
852 }
853 else {
854 if ( in_array( $key, $keepLineReturnsFields ) ) {
855 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
856 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
857 }
858 $value = sanitize_text_field( $value );
859 if ( in_array( $key, $keepLineReturnsFields ) ) {
860 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
861 }
862 }
863 }
864 }
865 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
866 error_log( 'AI Engine: Could not update chatbots.' );
867 $chatbots = get_option( $this->chatbots_option_name, [] );
868 return $chatbots;
869 }
870 return $chatbots;
871 }
872
873 function get_all_options( $force = false ) {
874 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
875 // That causes issues with the mwai_languages filter.
876 // if ( !$force && !is_null( $this->options ) ) {
877 // return $this->options;
878 // }
879 $options = get_option( $this->option_name, [] );
880 $options = $this->sanitize_options( $options );
881 foreach ( MWAI_OPTIONS as $key => $value ) {
882 if ( !isset( $options[$key] ) ) {
883 $options[$key] = $value;
884 }
885 if ( $key === 'languages' ) {
886 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
887 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
888 }
889 }
890 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
891 $options['default_limits'] = MWAI_LIMITS;
892 $options['openai_models'] = apply_filters(
893 'mwai_openai_models',
894 Meow_MWAI_Engines_OpenAI::get_models_static()
895 );
896 $options['anthropic_models'] = apply_filters(
897 'mwai_anthropic_models',
898 Meow_MWAI_Engines_Anthropic::get_models_static()
899 );
900 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
901
902 // Support for functions from Snippet Vault
903 $options['functions'] = apply_filters( 'mwai_functions_list', [] );
904
905 //$this->options = $options;
906 return $options;
907 }
908
909 // Sanitize options when we update the plugi or perform some updates
910 // if we change the structure of the options.
911 function sanitize_options( $options ) {
912 $needs_update = false;
913
914 // This list was updated on December 11, 2023. After May 2024, let's remove this.
915 $old_options = [
916 'shortcode_chat_default_params',
917 'shortcode_chat_params_override',
918 'module_legacy_finetunes',
919 'shortcode_chat_legacy',
920 'shortcode_chat_inject',
921 'shortcode_chat_styles',
922 'dynamic_max_tokens',
923 'shortcode_chat_formatting',
924 'shortcode_forms_legacy',
925 ];
926 foreach ( $old_options as $old_option ) {
927 if ( isset( $options[$old_option] ) ) {
928 unset( $options[$old_option] );
929 $needs_update = true;
930 }
931 }
932
933 // This upgrades namespace to multi-namespaces (June 2023)
934 // After January 2024, let's remove this.
935 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
936 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
937 unset( $options['pinecone']['namespace'] );
938 $needs_update = true;
939 }
940 // Support for Multi Vector DB Environments
941 // After June 2024, let's remove this.
942 if ( !isset( $options['embeddings_envs'] ) ) {
943 $options['embeddings_envs'] = [];
944 $default_id = $this->get_random_id();
945 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
946 $options['embeddings_envs'][] = [
947 'id' => $default_id,
948 'name' => 'Pinecone',
949 'type' => 'pinecone',
950 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
951 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
952 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
953 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
954 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
955 ];
956 $options['embeddings_default_env'] = $default_id;
957 $needs_update = true;
958 }
959 if ( isset( $options['pinecone'] ) ) {
960 unset( $options['pinecone'] );
961 $needs_update = true;
962 }
963 // Support for Multi AI Environments
964 // After June 2024, let's remove this.
965 if ( !isset( $options['ai_envs'] ) ) {
966 $options['ai_envs'] = [];
967 $default_openai_id = $this->get_random_id();
968 $default_azure_id = $this->get_random_id();
969 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
970 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
971 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
972
973 // OpenAI
974 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
975 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
976 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
977 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
978 $options['openai_finetunes_deleted'] : [];
979 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
980 $options['openai_legacy_finetunes'] : [];
981 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
982 $options['openai_legacy_finetunes_deleted'] : [];
983 $options['ai_envs'][] = [
984 'id' => $default_openai_id,
985 'name' => 'OpenAI',
986 'type' => 'openai',
987 'apikey' => $openai_apikey,
988 'finetunes' => $openai_finetunes,
989 'finetunes_deleted' => $openai_finetunes_deleted,
990 'legacy_finetunes' => $openai_legacy_finetunes,
991 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
992 ];
993 }
994
995 // Azure
996 if ( !empty( $azure_endpoint ) ) {
997 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
998 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
999 $options['ai_envs'][] = [
1000 'id' => $default_azure_id,
1001 'name' => 'Azure',
1002 'type' => 'azure',
1003 'apikey' => $azure_apikey,
1004 'endpoint' => $azure_endpoint,
1005 'deployments' => $azure_deployments,
1006 ];
1007 }
1008
1009 $options['ai_default_env'] = $default_openai_id;
1010 if ( $openai_service === 'azure' ) {
1011 $options['ai_default_env'] = $default_azure_id;
1012 }
1013 $needs_update = true;
1014 }
1015 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
1016 unset( $options['openai_apikey'] );
1017 unset( $options['openai_finetunes'] );
1018 unset( $options['openai_finetunes_deleted'] );
1019 unset( $options['openai_legacy_finetunes'] );
1020 unset( $options['openai_legacy_finetunes_deleted'] );
1021 unset( $options['openai_azure_apikey'] );
1022 unset( $options['openai_azure_endpoint'] );
1023 unset( $options['openai_azure_deployments'] );
1024 unset( $options['openai_service'] );
1025 $needs_update = true;
1026 }
1027
1028 // The IDs for the embeddings environments are generated here.
1029 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1030 $embeddings_default_exists = false;
1031 if ( isset( $options['embeddings_envs'] ) ) {
1032 foreach ( $options['embeddings_envs'] as &$env ) {
1033 if ( !isset( $env['id'] ) ) {
1034 $env['id'] = $this->get_random_id();
1035 $needs_update = true;
1036 }
1037 if ( $env['id'] === $options['embeddings_default_env'] ) {
1038 $embeddings_default_exists = true;
1039 }
1040 }
1041 }
1042 if ( !$embeddings_default_exists ) {
1043 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1044 $needs_update = true;
1045 }
1046
1047 // The IDs for the AI environments are generated here.
1048 $ai_default_exists = false;
1049 if ( isset( $options['ai_envs'] ) ) {
1050 foreach ( $options['ai_envs'] as &$env ) {
1051 if ( !isset( $env['id'] ) ) {
1052 $env['id'] = $this->get_random_id();
1053 $needs_update = true;
1054 }
1055 if ( $env['id'] === $options['ai_default_env'] ) {
1056 $ai_default_exists = true;
1057 }
1058 }
1059 }
1060 if ( !$ai_default_exists ) {
1061 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1062 $needs_update = true;
1063 }
1064
1065 if ( $needs_update ) {
1066 update_option( $this->option_name, $options, false );
1067 }
1068
1069 return $options;
1070 }
1071
1072 function update_options( $options ) {
1073 if ( !update_option( $this->option_name, $options, false ) ) {
1074 return false;
1075 }
1076 $options = $this->get_all_options( true );
1077 return $options;
1078 }
1079
1080 function update_option( $option, $value ) {
1081 $options = $this->get_all_options( true );
1082 $options[$option] = $value;
1083 return $this->update_options( $options );
1084 }
1085
1086 function get_option( $option, $default = null ) {
1087 $options = $this->get_all_options();
1088 return $options[$option] ?? $default;
1089 }
1090
1091 function update_ai_env( $env_id, $option, $value ) {
1092 $options = $this->get_all_options( true );
1093 foreach ( $options['ai_envs'] as &$env ) {
1094 if ( $env['id'] === $env_id ) {
1095 $env[$option] = $value;
1096 break;
1097 }
1098 }
1099 return $this->update_options( $options );
1100 }
1101
1102 function reset_options() {
1103 delete_option( $this->themes_option_name );
1104 delete_option( $this->chatbots_option_name );
1105 delete_option( $this->option_name );
1106 return $this->get_all_options( true );
1107 }
1108 #endregion
1109 }
1110
1111 ?>