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