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