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