PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.2.95
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.2.95
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
1104 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 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 }
751 }
752 if ( !$hasDefault ) {
753 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
754 array_unshift( $chatbots, $defaultBot );
755 $hasChanges = true;
756 }
757 if ( $hasChanges ) {
758 update_option( $this->chatbots_option_name, $chatbots );
759 }
760 return $chatbots;
761 }
762
763 function get_chatbot( $botId ) {
764 $chatbots = $this->get_chatbots();
765 foreach ( $chatbots as $chatbot ) {
766 if ( $chatbot['botId'] === (string)$botId ) {
767 return $chatbot;
768 }
769 }
770 return null;
771 }
772
773 function get_embeddings_env( $envId ) {
774 $envs = $this->get_option( 'embeddings_envs' );
775 foreach ( $envs as $env ) {
776 if ( $env['id'] === $envId ) {
777 return $env;
778 }
779 }
780 return null;
781 }
782
783 function get_ai_env( $envId ) {
784 $envs = $this->get_option( 'ai_envs' );
785 foreach ( $envs as $env ) {
786 if ( $env['id'] === $envId ) {
787 return $env;
788 }
789 }
790 return null;
791 }
792
793 function get_assistant( $envId, $assistantId ) {
794 $env = $this->get_ai_env( $envId );
795 if ( !$env ) {
796 return null;
797 }
798 $assistants = $env['assistants'];
799 foreach ( $assistants as $assistant ) {
800 if ( $assistant['id'] === $assistantId ) {
801 return $assistant;
802 }
803 }
804 return null;
805 }
806
807 function get_theme( $themeId ) {
808 $themes = $this->get_themes();
809 foreach ( $themes as $theme ) {
810 if ( $theme['themeId'] === $themeId ) {
811 return $theme;
812 }
813 }
814 return null;
815 }
816
817 function update_chatbots( $chatbots ) {
818 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
819 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
820 $keepLineReturnsFields = [ 'instructions' ];
821 $whiteSpacedFields = [ 'context' ];
822 foreach ( $chatbots as &$chatbot ) {
823 foreach ( $chatbot as $key => &$value ) {
824 if ( in_array( $key, $deprecatedFields ) ) {
825 unset( $chatbot[$key] );
826 continue;
827 }
828 if ( in_array( $key, $htmlFields ) ) {
829 $value = wp_kses_post( $value );
830 }
831 else if ( in_array( $key, $whiteSpacedFields ) ) {
832 $value = sanitize_textarea_field( $value );
833 }
834 else if ( $key === 'functions' ) {
835 $functions = [];
836 foreach ( $value as $function ) {
837 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
838 $functions[] = [
839 'id' => sanitize_text_field( $function['id'] ),
840 'type' => sanitize_text_field( $function['type'] ),
841 ];
842 }
843 }
844 $value = $functions;
845 }
846 else {
847 if ( in_array( $key, $keepLineReturnsFields ) ) {
848 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
849 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
850 }
851 $value = sanitize_text_field( $value );
852 if ( in_array( $key, $keepLineReturnsFields ) ) {
853 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
854 }
855 }
856 }
857 }
858 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
859 error_log( 'AI Engine: Could not update chatbots.' );
860 $chatbots = get_option( $this->chatbots_option_name, [] );
861 return $chatbots;
862 }
863 return $chatbots;
864 }
865
866 function get_all_options( $force = false ) {
867 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
868 // That causes issues with the mwai_languages filter.
869 // if ( !$force && !is_null( $this->options ) ) {
870 // return $this->options;
871 // }
872 $options = get_option( $this->option_name, [] );
873 $options = $this->sanitize_options( $options );
874 foreach ( MWAI_OPTIONS as $key => $value ) {
875 if ( !isset( $options[$key] ) ) {
876 $options[$key] = $value;
877 }
878 if ( $key === 'languages' ) {
879 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
880 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
881 }
882 }
883 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
884 $options['default_limits'] = MWAI_LIMITS;
885 $options['openai_models'] = apply_filters(
886 'mwai_openai_models',
887 Meow_MWAI_Engines_OpenAI::get_models_static()
888 );
889 $options['anthropic_models'] = apply_filters(
890 'mwai_anthropic_models',
891 Meow_MWAI_Engines_Anthropic::get_models_static()
892 );
893 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
894
895 // Support for functions from Snippet Vault
896 $options['functions'] = apply_filters( 'mwai_functions_list', [] );
897
898 //$this->options = $options;
899 return $options;
900 }
901
902 // Sanitize options when we update the plugi or perform some updates
903 // if we change the structure of the options.
904 function sanitize_options( $options ) {
905 $needs_update = false;
906
907 // This list was updated on December 11, 2023. After May 2024, let's remove this.
908 $old_options = [
909 'shortcode_chat_default_params',
910 'shortcode_chat_params_override',
911 'module_legacy_finetunes',
912 'shortcode_chat_legacy',
913 'shortcode_chat_inject',
914 'shortcode_chat_styles',
915 'dynamic_max_tokens',
916 'shortcode_chat_formatting',
917 'shortcode_forms_legacy',
918 ];
919 foreach ( $old_options as $old_option ) {
920 if ( isset( $options[$old_option] ) ) {
921 unset( $options[$old_option] );
922 $needs_update = true;
923 }
924 }
925
926 // This upgrades namespace to multi-namespaces (June 2023)
927 // After January 2024, let's remove this.
928 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
929 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
930 unset( $options['pinecone']['namespace'] );
931 $needs_update = true;
932 }
933 // Support for Multi Vector DB Environments
934 // After June 2024, let's remove this.
935 if ( !isset( $options['embeddings_envs'] ) ) {
936 $options['embeddings_envs'] = [];
937 $default_id = $this->get_random_id();
938 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
939 $options['embeddings_envs'][] = [
940 'id' => $default_id,
941 'name' => 'Pinecone',
942 'type' => 'pinecone',
943 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
944 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
945 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
946 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
947 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
948 ];
949 $options['embeddings_default_env'] = $default_id;
950 $needs_update = true;
951 }
952 if ( isset( $options['pinecone'] ) ) {
953 unset( $options['pinecone'] );
954 $needs_update = true;
955 }
956 // Support for Multi AI Environments
957 // After June 2024, let's remove this.
958 if ( !isset( $options['ai_envs'] ) ) {
959 $options['ai_envs'] = [];
960 $default_openai_id = $this->get_random_id();
961 $default_azure_id = $this->get_random_id();
962 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
963 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
964 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
965
966 // OpenAI
967 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
968 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
969 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
970 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
971 $options['openai_finetunes_deleted'] : [];
972 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
973 $options['openai_legacy_finetunes'] : [];
974 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
975 $options['openai_legacy_finetunes_deleted'] : [];
976 $options['ai_envs'][] = [
977 'id' => $default_openai_id,
978 'name' => 'OpenAI',
979 'type' => 'openai',
980 'apikey' => $openai_apikey,
981 'finetunes' => $openai_finetunes,
982 'finetunes_deleted' => $openai_finetunes_deleted,
983 'legacy_finetunes' => $openai_legacy_finetunes,
984 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
985 ];
986 }
987
988 // Azure
989 if ( !empty( $azure_endpoint ) ) {
990 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
991 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
992 $options['ai_envs'][] = [
993 'id' => $default_azure_id,
994 'name' => 'Azure',
995 'type' => 'azure',
996 'apikey' => $azure_apikey,
997 'endpoint' => $azure_endpoint,
998 'deployments' => $azure_deployments,
999 ];
1000 }
1001
1002 $options['ai_default_env'] = $default_openai_id;
1003 if ( $openai_service === 'azure' ) {
1004 $options['ai_default_env'] = $default_azure_id;
1005 }
1006 $needs_update = true;
1007 }
1008 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
1009 unset( $options['openai_apikey'] );
1010 unset( $options['openai_finetunes'] );
1011 unset( $options['openai_finetunes_deleted'] );
1012 unset( $options['openai_legacy_finetunes'] );
1013 unset( $options['openai_legacy_finetunes_deleted'] );
1014 unset( $options['openai_azure_apikey'] );
1015 unset( $options['openai_azure_endpoint'] );
1016 unset( $options['openai_azure_deployments'] );
1017 unset( $options['openai_service'] );
1018 $needs_update = true;
1019 }
1020
1021 // The IDs for the embeddings environments are generated here.
1022 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1023 $embeddings_default_exists = false;
1024 if ( isset( $options['embeddings_envs'] ) ) {
1025 foreach ( $options['embeddings_envs'] as &$env ) {
1026 if ( !isset( $env['id'] ) ) {
1027 $env['id'] = $this->get_random_id();
1028 $needs_update = true;
1029 }
1030 if ( $env['id'] === $options['embeddings_default_env'] ) {
1031 $embeddings_default_exists = true;
1032 }
1033 }
1034 }
1035 if ( !$embeddings_default_exists ) {
1036 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1037 $needs_update = true;
1038 }
1039
1040 // The IDs for the AI environments are generated here.
1041 $ai_default_exists = false;
1042 if ( isset( $options['ai_envs'] ) ) {
1043 foreach ( $options['ai_envs'] as &$env ) {
1044 if ( !isset( $env['id'] ) ) {
1045 $env['id'] = $this->get_random_id();
1046 $needs_update = true;
1047 }
1048 if ( $env['id'] === $options['ai_default_env'] ) {
1049 $ai_default_exists = true;
1050 }
1051 }
1052 }
1053 if ( !$ai_default_exists ) {
1054 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1055 $needs_update = true;
1056 }
1057
1058 if ( $needs_update ) {
1059 update_option( $this->option_name, $options, false );
1060 }
1061
1062 return $options;
1063 }
1064
1065 function update_options( $options ) {
1066 if ( !update_option( $this->option_name, $options, false ) ) {
1067 return false;
1068 }
1069 $options = $this->get_all_options( true );
1070 return $options;
1071 }
1072
1073 function update_option( $option, $value ) {
1074 $options = $this->get_all_options( true );
1075 $options[$option] = $value;
1076 return $this->update_options( $options );
1077 }
1078
1079 function get_option( $option, $default = null ) {
1080 $options = $this->get_all_options();
1081 return $options[$option] ?? $default;
1082 }
1083
1084 function update_ai_env( $env_id, $option, $value ) {
1085 $options = $this->get_all_options( true );
1086 foreach ( $options['ai_envs'] as &$env ) {
1087 if ( $env['id'] === $env_id ) {
1088 $env[$option] = $value;
1089 break;
1090 }
1091 }
1092 return $this->update_options( $options );
1093 }
1094
1095 function reset_options() {
1096 delete_option( $this->themes_option_name );
1097 delete_option( $this->chatbots_option_name );
1098 delete_option( $this->option_name );
1099 return $this->get_all_options( true );
1100 }
1101 #endregion
1102 }
1103
1104 ?>