PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.4.7
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.4.7
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 2 years ago core.php 1 year ago init.php 2 years ago reply.php 2 years ago rest.php 2 years ago
core.php
1251 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 $json = [];
946 $functions = apply_filters( 'mwai_functions_list', [] );
947 foreach ( $functions as $function ) {
948 $json[] = Meow_MWAI_Query_Function::toJson( $function );
949 }
950 $options['functions'] = $json;
951
952 // Addons
953 $options['addons'] = apply_filters( 'mwai_addons', [
954 [
955 'slug' => "mwai-notifications",
956 'name' => "Notifications",
957 'icon_url' => MeowCommon_Admin::$logo,
958 'description' => "Add-on for AI Engine that adds notifications.",
959 'install_url' => "https://meowapps.com/products/mwai-notifications/",
960 'settings_url' => null,
961 'enabled' => false,
962 ], [
963 'slug' => "mwai-ollama",
964 'name' => "Ollama",
965 'icon_url' => MeowCommon_Admin::$logo,
966 'description' => "Support for local LLMs via Ollama. Select the 'Ollama' type in your 'Environments for AI', then you can 'Refresh Models' and use them!",
967 'install_url' => "https://meowapps.com/products/mwai-ollama/",
968 'settings_url' => null,
969 'enabled' => false
970 ]
971 ] );
972
973 //$this->options = $options;
974 return $options;
975 }
976
977 // Sanitize options when we update the plugi or perform some updates
978 // if we change the structure of the options.
979 function sanitize_options( $options ) {
980 $needs_update = false;
981
982 // TODO: After October 2024, let's remove this.
983 $old_options = [
984 'openai_models',
985 'anthropic_models',
986 '${envType}_models',
987 'shortcode_chat_params',
988 'extra_models',
989 'fallback_model',
990 'mwai_advisor_data'
991 ];
992 foreach ( $old_options as $old_option ) {
993 if ( isset( $options[$old_option] ) ) {
994 unset( $options[$old_option] );
995 $needs_update = true;
996 }
997 }
998
999 // TODO: After October 2024, let's remove this.
1000 #region Temporary Code
1001 if ( isset( $options['openrouter_models'] ) ) {
1002 foreach ( $options['openrouter_models'] as $model ) {
1003 $model['envId'] = null;
1004 $model['type'] = 'openrouter';
1005 $options['ai_models'][] = $model;
1006 }
1007 $needs_update = true;
1008 unset( $options['openrouter_models'] );
1009 }
1010 if ( isset( $options['google_models'] ) ) {
1011 foreach ( $options['google_models'] as $model ) {
1012 $model['envId'] = null;
1013 $model['type'] = 'google';
1014 $options['ai_models'][] = $model;
1015 }
1016 $needs_update = true;
1017 unset( $options['google_models'] );
1018 }
1019 if ( isset( $options['shortcode_chat_stream'] ) ) {
1020 $options['ai_streaming'] = $options['shortcode_chat_stream'];
1021 unset( $options['shortcode_chat_stream'] );
1022 $needs_update = true;
1023 }
1024 if ( isset( $options['shortcode_chat_syntax_highlighting'] ) ) {
1025 $options['syntax_highlight'] = $options['shortcode_chat_syntax_highlighting'];
1026 unset( $options['shortcode_chat_syntax_highlighting'] );
1027 $needs_update = true;
1028 }
1029 if ( isset( $options['shortcode_chat_moderation'] ) ) {
1030 $options['chatbot_moderation'] = $options['shortcode_chat_moderation'];
1031 unset( $options['shortcode_chat_moderation'] );
1032 $needs_update = true;
1033 }
1034 if ( isset( $options['shortcode_chat_discussions'] ) ) {
1035 $options['chatbot_discussions'] = $options['shortcode_chat_discussions'];
1036 unset( $options['shortcode_chat_discussions'] );
1037 $needs_update = true;
1038 }
1039 if ( isset( $options['shortcode_chat_typewriter'] ) ) {
1040 $options['chatbot_typewriter'] = $options['shortcode_chat_typewriter'];
1041 unset( $options['shortcode_chat_typewriter'] );
1042 $needs_update = true;
1043 }
1044 if ( isset( $options['shortcode_chat'] ) ) {
1045 $options['module_chatbots'] = $options['shortcode_chat'];
1046 unset( $options['shortcode_chat'] );
1047 $needs_update = true;
1048 }
1049 if ( isset( $options['openai_usage'] ) ) {
1050 $options['ai_models_usage'] = $options['openai_usage'];
1051 unset( $options['openai_usage'] );
1052 $needs_update = true;
1053 }
1054 #endregion
1055
1056 // The IDs for the embeddings environments are generated here.
1057 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1058 $embeddings_default_exists = false;
1059 if ( isset( $options['embeddings_envs'] ) ) {
1060 foreach ( $options['embeddings_envs'] as &$env ) {
1061 if ( !isset( $env['id'] ) ) {
1062 $env['id'] = $this->get_random_id();
1063 $needs_update = true;
1064 }
1065 if ( $env['id'] === $options['embeddings_default_env'] ) {
1066 $embeddings_default_exists = true;
1067 }
1068 }
1069 }
1070 if ( !$embeddings_default_exists ) {
1071 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1072 $needs_update = true;
1073 }
1074
1075 // The IDs for the AI environments are generated here.
1076 $allEnvIds = [];
1077 $ai_default_exists = false;
1078 if ( isset( $options['ai_envs'] ) ) {
1079 foreach ( $options['ai_envs'] as &$env ) {
1080 if ( !isset( $env['id'] ) ) {
1081 $env['id'] = $this->get_random_id();
1082 $needs_update = true;
1083 }
1084 if ( $env['id'] === $options['ai_default_env'] ) {
1085 $ai_default_exists = true;
1086 }
1087 $allEnvIds[] = $env['id'];
1088 }
1089 }
1090 if ( !$ai_default_exists ) {
1091 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1092 $needs_update = true;
1093 }
1094
1095 // All the models with an envId that does not exist anymore are removed.
1096 if ( isset( $options['ai_models'] ) ) {
1097 $options['ai_models'] = array_values( array_filter( $options['ai_models'],
1098 function( $model ) use ( $allEnvIds, &$needs_update ) {
1099 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1100 $needs_update = true;
1101 return false;
1102 }
1103 return true;
1104 }
1105 ) );
1106 }
1107
1108 if ( $needs_update ) {
1109 update_option( $this->option_name, $options, false );
1110 }
1111
1112 return $options;
1113 }
1114
1115 function update_options( $options ) {
1116 if ( !update_option( $this->option_name, $options, false ) ) {
1117 return false;
1118 }
1119 $options = $this->get_all_options( true );
1120 return $options;
1121 }
1122
1123 function update_option( $option, $value ) {
1124 $options = $this->get_all_options( true );
1125 $options[$option] = $value;
1126 return $this->update_options( $options );
1127 }
1128
1129 function get_option( $option, $default = null ) {
1130 $options = $this->get_all_options();
1131 return $options[$option] ?? $default;
1132 }
1133
1134 function update_ai_env( $env_id, $option, $value ) {
1135 $options = $this->get_all_options( true );
1136 foreach ( $options['ai_envs'] as &$env ) {
1137 if ( $env['id'] === $env_id ) {
1138 $env[$option] = $value;
1139 break;
1140 }
1141 }
1142 return $this->update_options( $options );
1143 }
1144
1145 function get_engine_models( $engineType ) {
1146 $engines = $this->get_option( 'ai_engines' );
1147 foreach ( $engines as $engine ) {
1148 if ( $engine['type'] === $engineType ) {
1149 return isset( $engine['models'] ) ? $engine['models'] : [];
1150 }
1151 }
1152 return [];
1153 }
1154
1155 function reset_options() {
1156 delete_option( $this->themes_option_name );
1157 delete_option( $this->chatbots_option_name );
1158 delete_option( $this->option_name );
1159 return $this->get_all_options( true );
1160 }
1161 #endregion
1162
1163 #region Logs
1164
1165 function get_logs() {
1166 $log_file_path = $this->get_logs_path();
1167
1168 if ( !file_exists( $log_file_path ) ) {
1169 return "Empty log file.";
1170 }
1171
1172 $content = file_get_contents( $log_file_path );
1173 $lines = explode( "\n", $content );
1174 $lines = array_filter( $lines );
1175 $lines = array_reverse( $lines );
1176 $content = implode( "\n", $lines );
1177 return $content;
1178 }
1179
1180 function clear_logs() {
1181 $logPath = $this->get_logs_path();
1182 if ( file_exists( $logPath ) ) {
1183 unlink( $logPath );
1184 }
1185
1186 $options = $this->get_all_options();
1187 $options['logs_path'] = null;
1188 $this->update_options( $options );
1189 }
1190
1191 function get_logs_path() {
1192 $uploads_dir = wp_upload_dir();
1193 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
1194
1195 $path = $this->get_option( 'logs_path' );
1196
1197 if ( $path && file_exists( $path ) ) {
1198 // make sure the path is legal (within the uploads directory with the MWAI_PREFIX and log extension)
1199 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWAI_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
1200 $path = null;
1201 } else {
1202 return $path;
1203 }
1204 }
1205
1206 if ( !$path ) {
1207 $path = $uploads_dir_path . MWAI_PREFIX . "_" . $this->random_ascii_chars() . ".log";
1208 if ( !file_exists( $path ) ) {
1209 touch( $path );
1210 }
1211 $options = $this->get_all_options();
1212 $options['logs_path'] = $path;
1213 $this->update_options( $options );
1214 }
1215
1216 return $path;
1217 }
1218
1219 function log( $data = null ) {
1220 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
1221 $log_file_path = $this->get_logs_path();
1222 $fh = @fopen( $log_file_path, 'a' );
1223 if ( !$fh ) { return false; }
1224 $date = date( "Y-m-d H:i:s" );
1225 if ( is_null( $data ) ) {
1226 fwrite( $fh, "\n" );
1227 }
1228 else {
1229 fwrite( $fh, "$date: {$data}\n" );
1230 //error_log( "[MWAI] $data" );
1231 }
1232 fclose( $fh );
1233 return true;
1234 }
1235
1236 private function random_ascii_chars( $length = 8 ) {
1237 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
1238 $characters_length = count( $characters );
1239 $random_string = '';
1240
1241 for ( $i = 0; $i < $length; $i++ ) {
1242 $random_string .= $characters[rand(0, $characters_length - 1)];
1243 }
1244
1245 return $random_string;
1246 }
1247
1248 #endregion
1249 }
1250
1251 ?>