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