PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.4.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.4.4
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
1242 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 ];
733 $customThemes = [];
734 foreach ( $themes as $theme ) {
735 if ( isset( $internalThemes[$theme['themeId']] ) ) {
736 $internalThemes[$theme['themeId']] = $theme;
737 continue;
738 }
739 $customThemes[] = $theme;
740 }
741 return array_merge(array_values($internalThemes), $customThemes);
742 }
743
744 function update_themes( $themes ) {
745 update_option( $this->themes_option_name, $themes );
746 return $themes;
747 }
748
749 function get_chatbots() {
750 $chatbots = get_option( $this->chatbots_option_name, [] );
751 $hasChanges = false;
752 if ( empty( $chatbots ) ) {
753 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
754 }
755 $hasDefault = false;
756 foreach ( $chatbots as &$chatbot ) {
757 if ( $chatbot['botId'] === 'default' ) {
758 $hasDefault = true;
759 }
760 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
761 // Use default value if not set.
762 if ( !isset( $chatbot[$key] ) ) {
763 $chatbot[$key] = $value;
764 }
765 }
766 // TODO: After October 2024, let's remove this.
767 if ( isset( $chatbot['context'] ) ) {
768 $chatbot['instructions'] = $chatbot['context'];
769 unset( $chatbot['context'] );
770 $hasChanges = true;
771 }
772 // TODO: After October 2024, let's remove this.
773 if ( isset( $chatbot['fileUpload'] ) ) {
774 $chatbot['fileSearch'] = $chatbot['fileUpload'];
775 unset( $chatbot['fileUpload'] );
776 $hasChanges = true;
777 }
778 }
779 if ( !$hasDefault ) {
780 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
781 array_unshift( $chatbots, $defaultBot );
782 $hasChanges = true;
783 }
784 if ( $hasChanges ) {
785 update_option( $this->chatbots_option_name, $chatbots );
786 }
787 return $chatbots;
788 }
789
790 function get_chatbot( $botId ) {
791 $chatbots = $this->get_chatbots();
792 foreach ( $chatbots as $chatbot ) {
793 if ( $chatbot['botId'] === (string)$botId ) {
794 return $chatbot;
795 }
796 }
797 return null;
798 }
799
800 function get_embeddings_env( $envId ) {
801 $envs = $this->get_option( 'embeddings_envs' );
802 foreach ( $envs as $env ) {
803 if ( $env['id'] === $envId ) {
804 return $env;
805 }
806 }
807 return null;
808 }
809
810 function get_ai_env( $envId ) {
811 $envs = $this->get_option( 'ai_envs' );
812 foreach ( $envs as $env ) {
813 if ( $env['id'] === $envId ) {
814 return $env;
815 }
816 }
817 return null;
818 }
819
820 function get_assistant( $envId, $assistantId ) {
821 $env = $this->get_ai_env( $envId );
822 if ( !$env ) {
823 return null;
824 }
825 $assistants = $env['assistants'];
826 foreach ( $assistants as $assistant ) {
827 if ( $assistant['id'] === $assistantId ) {
828 return $assistant;
829 }
830 }
831 return null;
832 }
833
834 function get_theme( $themeId ) {
835 $themes = $this->get_themes();
836 foreach ( $themes as $theme ) {
837 if ( $theme['themeId'] === $themeId ) {
838 return $theme;
839 }
840 }
841 return null;
842 }
843
844 function update_chatbots( $chatbots ) {
845 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
846 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
847 $keepLineReturnsFields = [ 'instructions' ];
848 $whiteSpacedFields = [ 'context' ];
849 foreach ( $chatbots as &$chatbot ) {
850 foreach ( $chatbot as $key => &$value ) {
851 if ( in_array( $key, $deprecatedFields ) ) {
852 unset( $chatbot[$key] );
853 continue;
854 }
855 if ( in_array( $key, $htmlFields ) ) {
856 $value = wp_kses_post( $value );
857 }
858 else if ( in_array( $key, $whiteSpacedFields ) ) {
859 $value = sanitize_textarea_field( $value );
860 }
861 else if ( $key === 'functions' ) {
862 $functions = [];
863 foreach ( $value as $function ) {
864 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
865 $functions[] = [
866 'id' => sanitize_text_field( $function['id'] ),
867 'type' => sanitize_text_field( $function['type'] ),
868 ];
869 }
870 }
871 $value = $functions;
872 }
873 else {
874 if ( in_array( $key, $keepLineReturnsFields ) ) {
875 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
876 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
877 }
878 $value = sanitize_text_field( $value );
879 if ( in_array( $key, $keepLineReturnsFields ) ) {
880 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
881 }
882 }
883 }
884 }
885 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
886 $this->log( '⚠️ Could not update chatbots.' );
887 $chatbots = get_option( $this->chatbots_option_name, [] );
888 return $chatbots;
889 }
890 return $chatbots;
891 }
892
893 function get_all_options( $force = false ) {
894 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
895 // That causes issues with the mwai_languages filter.
896 // if ( !$force && !is_null( $this->options ) ) {
897 // return $this->options;
898 // }
899 $options = get_option( $this->option_name, [] );
900 $options = $this->sanitize_options( $options );
901 foreach ( MWAI_OPTIONS as $key => $value ) {
902 if ( !isset( $options[$key] ) ) {
903 $options[$key] = $value;
904 }
905 if ( $key === 'languages' ) {
906 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
907 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
908 }
909 }
910 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
911 $options['default_limits'] = MWAI_LIMITS;
912
913 // Consolidate the engines and the models inside them
914 // we should ABSOLUTELY AVOID to use ai_models directly (except for saving).
915 // An engine looks like that:
916 // [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
917 // NOTE: Since the models are consolidated with the envId in ai_engines,
918 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
919 foreach ( $options['ai_engines'] as &$engine ) {
920 if ( $engine['type'] === 'openai' ) {
921 $engine['models'] = apply_filters( 'mwai_openai_models',
922 Meow_MWAI_Engines_OpenAI::get_models_static()
923 );
924 }
925 else if ( $engine['type'] === 'anthropic' ) {
926 $engine['models'] = apply_filters( 'mwai_anthropic_models',
927 Meow_MWAI_Engines_Anthropic::get_models_static()
928 );
929 }
930 else {
931 $engine['models'] = [];
932 foreach ( $options['ai_models'] as $model ) {
933 if ( $model['type'] === $engine['type'] ) {
934 $engine['models'][] = $model;
935 }
936 }
937 }
938 }
939
940 // Support for functions via Snippet Vault
941 $options['functions'] = apply_filters( 'mwai_functions_list', [] );
942
943 // Addons
944 $options['addons'] = apply_filters( 'mwai_addons', [
945 [
946 'slug' => "mwai-notifications",
947 'name' => "Notifications",
948 'icon_url' => MeowCommon_Admin::$logo,
949 'description' => "Add-on for AI Engine that adds notifications.",
950 'install_url' => "https://meowapps.com/products/mwai-notifications/",
951 'settings_url' => null,
952 'enabled' => false,
953 ], [
954 'slug' => "mwai-ollama",
955 'name' => "Ollama",
956 'icon_url' => MeowCommon_Admin::$logo,
957 'description' => "Support for local LLMs via Ollama. Select the 'Ollama' type in your 'Environments for AI', then you can 'Refresh Models' and use them!",
958 'install_url' => "https://meowapps.com/products/mwai-ollama/",
959 'settings_url' => null,
960 'enabled' => false
961 ]
962 ] );
963
964 //$this->options = $options;
965 return $options;
966 }
967
968 // Sanitize options when we update the plugi or perform some updates
969 // if we change the structure of the options.
970 function sanitize_options( $options ) {
971 $needs_update = false;
972
973 // TODO: After October 2024, let's remove this.
974 $old_options = [
975 'openai_models',
976 'anthropic_models',
977 '${envType}_models',
978 'shortcode_chat_params',
979 'extra_models',
980 'fallback_model',
981 'mwai_advisor_data'
982 ];
983 foreach ( $old_options as $old_option ) {
984 if ( isset( $options[$old_option] ) ) {
985 unset( $options[$old_option] );
986 $needs_update = true;
987 }
988 }
989
990 // TODO: After October 2024, let's remove this.
991 #region Temporary Code
992 if ( isset( $options['openrouter_models'] ) ) {
993 foreach ( $options['openrouter_models'] as $model ) {
994 $model['envId'] = null;
995 $model['type'] = 'openrouter';
996 $options['ai_models'][] = $model;
997 }
998 $needs_update = true;
999 unset( $options['openrouter_models'] );
1000 }
1001 if ( isset( $options['google_models'] ) ) {
1002 foreach ( $options['google_models'] as $model ) {
1003 $model['envId'] = null;
1004 $model['type'] = 'google';
1005 $options['ai_models'][] = $model;
1006 }
1007 $needs_update = true;
1008 unset( $options['google_models'] );
1009 }
1010 if ( isset( $options['shortcode_chat_stream'] ) ) {
1011 $options['ai_streaming'] = $options['shortcode_chat_stream'];
1012 unset( $options['shortcode_chat_stream'] );
1013 $needs_update = true;
1014 }
1015 if ( isset( $options['shortcode_chat_syntax_highlighting'] ) ) {
1016 $options['syntax_highlight'] = $options['shortcode_chat_syntax_highlighting'];
1017 unset( $options['shortcode_chat_syntax_highlighting'] );
1018 $needs_update = true;
1019 }
1020 if ( isset( $options['shortcode_chat_moderation'] ) ) {
1021 $options['chatbot_moderation'] = $options['shortcode_chat_moderation'];
1022 unset( $options['shortcode_chat_moderation'] );
1023 $needs_update = true;
1024 }
1025 if ( isset( $options['shortcode_chat_discussions'] ) ) {
1026 $options['chatbot_discussions'] = $options['shortcode_chat_discussions'];
1027 unset( $options['shortcode_chat_discussions'] );
1028 $needs_update = true;
1029 }
1030 if ( isset( $options['shortcode_chat_typewriter'] ) ) {
1031 $options['chatbot_typewriter'] = $options['shortcode_chat_typewriter'];
1032 unset( $options['shortcode_chat_typewriter'] );
1033 $needs_update = true;
1034 }
1035 if ( isset( $options['shortcode_chat'] ) ) {
1036 $options['module_chatbots'] = $options['shortcode_chat'];
1037 unset( $options['shortcode_chat'] );
1038 $needs_update = true;
1039 }
1040 if ( isset( $options['openai_usage'] ) ) {
1041 $options['ai_models_usage'] = $options['openai_usage'];
1042 unset( $options['openai_usage'] );
1043 $needs_update = true;
1044 }
1045 #endregion
1046
1047 // The IDs for the embeddings environments are generated here.
1048 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1049 $embeddings_default_exists = false;
1050 if ( isset( $options['embeddings_envs'] ) ) {
1051 foreach ( $options['embeddings_envs'] as &$env ) {
1052 if ( !isset( $env['id'] ) ) {
1053 $env['id'] = $this->get_random_id();
1054 $needs_update = true;
1055 }
1056 if ( $env['id'] === $options['embeddings_default_env'] ) {
1057 $embeddings_default_exists = true;
1058 }
1059 }
1060 }
1061 if ( !$embeddings_default_exists ) {
1062 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1063 $needs_update = true;
1064 }
1065
1066 // The IDs for the AI environments are generated here.
1067 $allEnvIds = [];
1068 $ai_default_exists = false;
1069 if ( isset( $options['ai_envs'] ) ) {
1070 foreach ( $options['ai_envs'] as &$env ) {
1071 if ( !isset( $env['id'] ) ) {
1072 $env['id'] = $this->get_random_id();
1073 $needs_update = true;
1074 }
1075 if ( $env['id'] === $options['ai_default_env'] ) {
1076 $ai_default_exists = true;
1077 }
1078 $allEnvIds[] = $env['id'];
1079 }
1080 }
1081 if ( !$ai_default_exists ) {
1082 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1083 $needs_update = true;
1084 }
1085
1086 // All the models with an envId that does not exist anymore are removed.
1087 if ( isset( $options['ai_models'] ) ) {
1088 $options['ai_models'] = array_values( array_filter( $options['ai_models'],
1089 function( $model ) use ( $allEnvIds, &$needs_update ) {
1090 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1091 $needs_update = true;
1092 return false;
1093 }
1094 return true;
1095 }
1096 ) );
1097 }
1098
1099 if ( $needs_update ) {
1100 update_option( $this->option_name, $options, false );
1101 }
1102
1103 return $options;
1104 }
1105
1106 function update_options( $options ) {
1107 if ( !update_option( $this->option_name, $options, false ) ) {
1108 return false;
1109 }
1110 $options = $this->get_all_options( true );
1111 return $options;
1112 }
1113
1114 function update_option( $option, $value ) {
1115 $options = $this->get_all_options( true );
1116 $options[$option] = $value;
1117 return $this->update_options( $options );
1118 }
1119
1120 function get_option( $option, $default = null ) {
1121 $options = $this->get_all_options();
1122 return $options[$option] ?? $default;
1123 }
1124
1125 function update_ai_env( $env_id, $option, $value ) {
1126 $options = $this->get_all_options( true );
1127 foreach ( $options['ai_envs'] as &$env ) {
1128 if ( $env['id'] === $env_id ) {
1129 $env[$option] = $value;
1130 break;
1131 }
1132 }
1133 return $this->update_options( $options );
1134 }
1135
1136 function get_engine_models( $engineType ) {
1137 $engines = $this->get_option( 'ai_engines' );
1138 foreach ( $engines as $engine ) {
1139 if ( $engine['type'] === $engineType ) {
1140 return isset( $engine['models'] ) ? $engine['models'] : [];
1141 }
1142 }
1143 return [];
1144 }
1145
1146 function reset_options() {
1147 delete_option( $this->themes_option_name );
1148 delete_option( $this->chatbots_option_name );
1149 delete_option( $this->option_name );
1150 return $this->get_all_options( true );
1151 }
1152 #endregion
1153
1154 #region Logs
1155
1156 function get_logs() {
1157 $log_file_path = $this->get_logs_path();
1158
1159 if ( !file_exists( $log_file_path ) ) {
1160 return "Empty log file.";
1161 }
1162
1163 $content = file_get_contents( $log_file_path );
1164 $lines = explode( "\n", $content );
1165 $lines = array_filter( $lines );
1166 $lines = array_reverse( $lines );
1167 $content = implode( "\n", $lines );
1168 return $content;
1169 }
1170
1171 function clear_logs() {
1172 $logPath = $this->get_logs_path();
1173 if ( file_exists( $logPath ) ) {
1174 unlink( $logPath );
1175 }
1176
1177 $options = $this->get_all_options();
1178 $options['logs_path'] = null;
1179 $this->update_options( $options );
1180 }
1181
1182 function get_logs_path() {
1183 $uploads_dir = wp_upload_dir();
1184 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
1185
1186 $path = $this->get_option( 'logs_path' );
1187
1188 if ( $path && file_exists( $path ) ) {
1189 // make sure the path is legal (within the uploads directory with the MWAI_PREFIX and log extension)
1190 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWAI_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
1191 $path = null;
1192 } else {
1193 return $path;
1194 }
1195 }
1196
1197 if ( !$path ) {
1198 $path = $uploads_dir_path . MWAI_PREFIX . "_" . $this->random_ascii_chars() . ".log";
1199 if ( !file_exists( $path ) ) {
1200 touch( $path );
1201 }
1202 $options = $this->get_all_options();
1203 $options['logs_path'] = $path;
1204 $this->update_options( $options );
1205 }
1206
1207 return $path;
1208 }
1209
1210 function log( $data = null ) {
1211 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
1212 $log_file_path = $this->get_logs_path();
1213 $fh = @fopen( $log_file_path, 'a' );
1214 if ( !$fh ) { return false; }
1215 $date = date( "Y-m-d H:i:s" );
1216 if ( is_null( $data ) ) {
1217 fwrite( $fh, "\n" );
1218 }
1219 else {
1220 fwrite( $fh, "$date: {$data}\n" );
1221 //error_log( "[MWAI] $data" );
1222 }
1223 fclose( $fh );
1224 return true;
1225 }
1226
1227 private function random_ascii_chars( $length = 8 ) {
1228 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
1229 $characters_length = count( $characters );
1230 $random_string = '';
1231
1232 for ( $i = 0; $i < $length; $i++ ) {
1233 $random_string .= $characters[rand(0, $characters_length - 1)];
1234 }
1235
1236 return $random_string;
1237 }
1238
1239 #endregion
1240 }
1241
1242 ?>