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