PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.2
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / core.php
ai-engine / classes Last commit date
engines 1 year ago modules 1 year ago queries 1 year ago admin.php 2 years ago api.php 1 year ago core.php 1 year ago init.php 2 years ago reply.php 1 year ago rest.php 1 year ago
core.php
1349 lines
1 <?php
2
3 require_once( MWAI_PATH . '/vendor/autoload.php' );
4 require_once( MWAI_PATH . '/constants/init.php' );
5
6 define( 'MWAI_IMG_WAND', MWAI_URL . '/images/wand.png' );
7 define( 'MWAI_IMG_WAND_HTML', "<img style='height: 22px; margin-bottom: -5px; margin-right: 8px;'
8 src='" . MWAI_IMG_WAND . "' alt='AI Wand' />" );
9 define( 'MWAI_IMG_WAND_HTML_XS', "<img style='height: 16px; margin-bottom: -2px;'
10 src='" . MWAI_IMG_WAND . "' alt='AI Wand' />" );
11
12 class Meow_MWAI_Core
13 {
14 public $admin = null;
15 public $is_rest = false;
16 public $is_cli = false;
17 public $site_url = null;
18 public $files = null;
19 public $tasks = null;
20 public $magicWand = null;
21 private $options = null;
22 private $option_name = 'mwai_options';
23 private $themes_option_name = 'mwai_themes';
24 private $chatbots_option_name = 'mwai_chatbots';
25 private $nonce = null;
26
27 public $chatbot = null;
28 public $discussions = null;
29
30 public function __construct() {
31 $this->site_url = get_site_url();
32 $this->is_rest = MeowCommon_Helpers::is_rest();
33 $this->is_cli = defined( 'WP_CLI' );
34 $this->files = new Meow_MWAI_Modules_Files( $this );
35 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
36
37 add_action( 'plugins_loaded', array( $this, 'init' ) );
38 add_action( 'wp_register_script', array( $this, 'register_scripts' ) );
39 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
40 add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
41 }
42
43 #region Init & Scripts
44 function init() {
45 global $mwai;
46 $this->chatbot = null;
47 $this->discussions = null;
48 new Meow_MWAI_Modules_Security( $this );
49
50 // REST API
51 if ( $this->is_rest ) {
52 new Meow_MWAI_Rest( $this );
53 }
54
55 // WP Admin
56 if ( is_admin() ) {
57 new Meow_MWAI_Admin( $this );
58 }
59
60 // GDPR Module
61 if ( $this->get_option( 'chatbot_gdpr_consent' ) ) {
62 new Meow_MWAI_Modules_GDPR( $this );
63 }
64
65 // Suggestions Module
66 if ( $this->get_option( 'module_suggestions' ) && ( is_admin() || $this->is_rest ) ) {
67 $this->magicWand = new Meow_MWAI_Modules_Wand( $this );
68 }
69
70 // Administrator in WP Admin
71 if ( is_admin() && current_user_can( 'manage_options' ) ) {
72 $module_advisor = $this->get_option( 'module_advisor' );
73 if ( $module_advisor ) {
74 new Meow_MWAI_Modules_Advisor( $this );
75 }
76 }
77
78 // Chatbots & Discussions
79 if ( $this->get_option( 'module_chatbots' ) ) {
80 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
81 $this->discussions = new Meow_MWAI_Modules_Discussions();
82 }
83
84 // Advanced Core
85 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
86 new MeowPro_MWAI_Core( $this );
87 }
88
89 // Simple API
90 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
91 }
92
93 public function register_scripts() {
94 // Register Highlight.js
95 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
96 // Register CSS for the themes
97 $themes = $this->get_themes();
98 foreach ( $themes as $theme ) {
99 if ( $theme['type'] === 'internal' ) {
100 $themeId = $theme['themeId'];
101 $filename = $themeId . '.css';
102 $physical_file = trailingslashit( MWAI_PATH ) . 'themes/' . $filename;
103 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
104 wp_register_style( 'mwai_chatbot_theme_' . $themeId, trailingslashit( MWAI_URL )
105 . 'themes/' . $filename, [], $cache_buster );
106 }
107 }
108 }
109
110 public function enqueue_themes() {
111 // TODO: We should optimize and only load the themes that are used.
112 $themes = $this->get_themes();
113 foreach ( $themes as $theme ) {
114 if ( $theme['type'] === 'internal' ) {
115 $themeId = $theme['themeId'];
116 wp_enqueue_style( "mwai_chatbot_theme_$themeId" );
117 }
118 }
119 }
120
121 #endregion
122
123 #region Roles & Capabilities
124 function can_start_session() {
125 return apply_filters( 'mwai_allow_session', true );
126 }
127
128 function can_access_settings() {
129 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
130 }
131
132 function can_access_features() {
133 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
134 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
135 }
136
137 function can_access_public_api( $feature, $extra ) {
138 $logged_in = is_user_logged_in();
139 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
140 }
141
142 #endregion
143
144 #region AI-Related Helpers
145 function run_query( $query, $streamCallback = null, $markdown = false ) {
146 $envId = !empty( $query->envId ) ? $query->envId : null;
147 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
148
149 // If the engine is not set, we need to set it to the default one.
150 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
151 if ( $query instanceof Meow_MWAI_Query_Text ) {
152 $this->set_if_empty_defaults( $query, 'ai_default_env', 'ai_default_model' );
153 }
154 if ( $query instanceof Meow_MWAI_Query_Embed ) {
155 $this->set_if_empty_defaults( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
156 }
157 else if ( $query instanceof Meow_MWAI_Query_Image ) {
158 $this->set_if_empty_defaults( $query, 'ai_images_default_env', 'ai_images_default_model' );
159 }
160 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
161 $this->set_if_empty_defaults( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
162 }
163 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
164 }
165
166 // Let's run the query.
167 $reply = $engine->run( $query, $streamCallback );
168
169 // Let's allow to modify the reply before it is sent.
170 if ( $markdown ) {
171 if ( $query instanceof Meow_MWAI_Query_Image ) {
172 $reply->result = "";
173 foreach ( $reply->results as $result ) {
174 $reply->result .= "![Image]($result)\n";
175 }
176 }
177 }
178
179 return $reply;
180 }
181
182 private function set_if_empty_defaults( $query, $envOption, $modelOption ) {
183 $defaultEnv = $this->get_option( $envOption );
184 $defaultModel = $this->get_option( $modelOption );
185 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
186 throw new Exception( 'AI Engine: The default environment and model are not set.' );
187 }
188 $query->set_env_id( $defaultEnv );
189 $query->set_model( $defaultModel );
190 }
191 #endregion
192
193 #region Text-Related Helpers
194
195 // Clean the text perfectly, resolve shortcodes, etc, etc.
196 function clean_text( $rawText = "" ) {
197 $text = html_entity_decode( $rawText );
198 $text = wp_strip_all_tags( $text );
199 $text = preg_replace( '/[\r\n]+/', "\n", $text );
200 $text = preg_replace( '/\n+/', "\n", $text );
201 $text = preg_replace( '/\t+/', "\t", $text );
202 return $text . " ";
203 }
204
205 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
206 function clean_sentences( $text, $maxLength = null ) {
207 // Step 1: Identify URLs and replace them with a placeholder.
208 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
209 preg_match_all($urlPattern, $text, $urls);
210 $urlPlaceholders = array();
211 foreach ($urls[0] as $index => $url) {
212 $placeholder = "{urlPlaceholder" . $index . "}";
213 $text = str_replace($url, $placeholder, $text);
214 $urlPlaceholders[$placeholder] = $url;
215 }
216
217 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
218 $sentences = preg_split('/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
219 $hashes = array();
220 $uniqueSentences = array();
221 $total = 0;
222
223 foreach ( $sentences as $sentence ) {
224 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
225 $hash = md5( $sentence );
226 if ( !in_array( $hash, $hashes ) ) {
227 $length = mb_strlen( $sentence, 'UTF-8' );
228 if ( $total + $length > $maxLength ) {
229 continue;
230 }
231 $hashes[] = $hash;
232 $uniqueSentences[] = $sentence;
233 $total += $length;
234 }
235 }
236
237 $freshText = implode( " ", $uniqueSentences );
238
239 // Step 3: Restore URLs in the final text.
240 foreach ($urlPlaceholders as $placeholder => $url) {
241 $freshText = str_replace($placeholder, $url, $freshText);
242 }
243
244 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
245 return $freshText;
246 }
247
248 function get_post_content( $postId ) {
249 $post = get_post( $postId );
250 if ( !$post ) {
251 return false;
252 }
253 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
254 $pattern = '/\[mwai_.*?\]/';
255 $text = preg_replace( $pattern, '', $text );
256 if ( $this->get_option( 'resolve_shortcodes' ) ) {
257 $text = apply_filters( 'the_content', $text );
258 }
259 else {
260 $pattern = "/\[[^\]]+\]/";
261 $text = preg_replace( $pattern, '', $text );
262 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
263 $text = preg_replace( $pattern, '', $text );
264 }
265 $text = $this->clean_text( $text );
266 $text = $this->clean_sentences( $text );
267 $text = apply_filters( 'mwai_post_content', $text, $postId );
268 return $text;
269 }
270
271 function markdown_to_html( $content ) {
272 $Parsedown = new Parsedown();
273 $content = $Parsedown->text( $content );
274 return $content;
275 }
276
277 function get_post_language( $postId ) {
278 $locale = get_locale();
279 $code = strtolower( substr( $locale, 0, 2 ) );
280 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
281 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
282 if ( !empty( $lang ) ) {
283 $locale = $lang['locale'];
284 $humanLanguage = $lang['display_name'];
285 }
286 return strtolower( "$locale ($humanLanguage)" );
287 }
288 #endregion
289
290 #region Image-Related Helpers
291 static function is_image( $file ) {
292 $mimeType = Meow_MWAI_Core::get_mime_type( $file );
293 if ( strpos( $mimeType, 'image' ) !== false ) {
294 return true;
295 }
296 return false;
297 }
298
299 static function get_mime_type( $file ) {
300 $mimeType = null;
301
302 // Let's try to use mime_content_type if the function exists
303 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
304 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
305 $mimeType = mime_content_type( $file );
306 }
307
308 // Otherwise, let's check the file extension (which can actually also be an URL)
309 if ( !$mimeType ) {
310 $extension = pathinfo( $file, PATHINFO_EXTENSION );
311 $extension = strtolower( $extension );
312 $mimeTypes = [
313 'jpg' => 'image/jpeg',
314 'jpeg' => 'image/jpeg',
315 'png' => 'image/png',
316 'gif' => 'image/gif',
317 'webp' => 'image/webp',
318 'bmp' => 'image/bmp',
319 'tiff' => 'image/tiff',
320 'tif' => 'image/tiff',
321 'svg' => 'image/svg+xml',
322 'ico' => 'image/x-icon',
323 'pdf' => 'application/pdf',
324 ];
325 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
326 }
327
328 return $mimeType;
329 }
330
331 function download_image( $url ) {
332 $args = array( 'timeout' => 60, );
333 $response = wp_safe_remote_get( $url, $args );
334 if ( is_wp_error( $response ) ) {
335 throw new Exception( $response->get_error_message() );
336 }
337 $output = wp_remote_retrieve_body( $response );
338 if ( is_wp_error( $output ) ) {
339 throw new Exception( $output->get_error_message() );
340 }
341 return $output;
342 }
343
344 /**
345 * Add an image from a URL to the Media Library.
346 * @param string $url The URL of the image to be downloaded.
347 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
348 * @param string $title The title of the image.
349 * @param string $description The description of the image.
350 * @param string $caption The caption of the image.
351 * @param string $alt The alt text of the image.
352 * @return int The attachment ID of the image.
353 */
354 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null ) {
355 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
356 $url_filename = $path_parts['basename'];
357 $file_type = wp_check_filetype( $url_filename, null );
358 $allowed_types = get_allowed_mime_types();
359 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
360 throw new Exception( 'Invalid file type from URL.' );
361 }
362
363 // Initial extension from URL file name
364 $extension = $file_type['ext'];
365
366 if ( !empty( $filename ) ) {
367 $custom_file_type = wp_check_filetype( $filename, null );
368 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
369 throw new Exception( 'Invalid custom file type.' );
370 }
371 // Use the extension from the custom filename if valid
372 $extension = $custom_file_type['ext'];
373 }
374
375 $image_data = $this->download_image( $url );
376 if ( !$image_data ) {
377 throw new Exception( 'Could not download the image.' );
378 }
379 $upload_dir = wp_upload_dir();
380
381 // Filename handling including 'generated_' prefix scenario
382 if ( empty( $filename ) ) {
383 $filename = sanitize_file_name( $url_filename );
384 if ( empty( $extension ) ) { // This condition might now be redundant
385 $extension = $file_type['ext'];
386 }
387 // Filename length check and prepend if conditions met
388 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
389 $filename = $this->get_random_id( 16 ) . '.' . $extension;
390 }
391 if ( strpos( $filename, '.' ) === false ) {
392 $filename .= '.' . $extension;
393 }
394 }
395
396 // Directory and file path handling
397 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
398 $file = $upload_dir['path'] . '/' . $filename;
399 }
400 else {
401 $file = $upload_dir['basedir'] . '/' . $filename;
402 }
403
404 // Ensure file name uniqueness in the directory
405 $i = 1;
406 $parts = pathinfo( $file );
407 while ( file_exists( $file ) ) {
408 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
409 $i++;
410 }
411
412 // Writing the file to disk
413 file_put_contents( $file, $image_data );
414
415 // Attachment and metadata handling in WP
416 $attachment = [
417 'post_mime_type' => $file_type['type'],
418 'post_title' => $title ?? '',
419 'post_content' => $description ?? '',
420 'post_excerpt' => $caption ?? '',
421 'post_status' => 'inherit'
422 ];
423 $attachmentId = wp_insert_attachment( $attachment, $file );
424 require_once( ABSPATH . 'wp-admin/includes/image.php' );
425 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
426 wp_update_attachment_metadata( $attachmentId, $attachment_data );
427 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
428
429 return $attachmentId;
430 }
431 #endregion
432
433 #region Context-Related Helpers
434 function retrieve_context( $params, $query ) {
435 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
436 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
437 $context = apply_filters( 'mwai_context_search', [], $query, [
438 'embeddingsEnvId' => $embeddingsEnvId
439 ]);
440 if ( empty( $context ) ) {
441 return null;
442 }
443 else if ( !isset( $context['content'] ) ) {
444 $this->log( "⚠️ A context without content was returned." );
445 return null;
446 }
447 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
448 $context['length'] = strlen( $context['content'] );
449 return $context;
450 }
451 #endregion
452
453 #region Users/Sessions Helpers
454
455 function get_nonce( $force = false ) {
456 if ( !$force && !is_user_logged_in() ) {
457 return null;
458 }
459 if ( isset( $this->nonce ) ) {
460 return $this->nonce;
461 }
462 $this->nonce = wp_create_nonce( 'wp_rest' );
463 return $this->nonce;
464 }
465
466 // This is a bit hacky, but chatId needs to be retrieved or generated.
467 // Maybe we can clean this up later.
468 function fix_chat_id( $query, $params ) {
469 if ( isset( $query->chatId ) && $query->chatId !== 'N/A' ) {
470 return $query->chatId;
471 }
472 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : $query->session;
473 if ( $chatId === 'N/A' ) {
474 $chatId = $this->get_random_id( 8 );
475 }
476 $query->set_chat_id( $chatId );
477 return $chatId;
478 }
479
480 function get_session_id() {
481 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
482 return $_COOKIE['mwai_session_id'];
483 }
484 return "N/A";
485 }
486
487 // Get the UserID from the data, or from the current user
488 function get_user_id( $data = null ) {
489 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
490 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
491 $admin = get_users( [ 'role' => 'administrator' ] );
492 if ( !empty( $admin ) ) {
493 return $admin[0]->ID;
494 }
495 }
496 if ( isset( $data ) && isset( $data['userId'] ) ) {
497 return (int)$data['userId'];
498 }
499 if ( is_user_logged_in() ) {
500 $current_user = wp_get_current_user();
501 if ( $current_user->ID > 0 ) {
502 return $current_user->ID;
503 }
504 }
505 return null;
506 }
507
508 function get_admin_user() {
509 $admin = get_users( [ 'role' => 'administrator' ] );
510 if ( !empty( $admin ) ) {
511 return $admin[0];
512 }
513 return null;
514 }
515
516 function get_user_data() {
517 $user = wp_get_current_user();
518 if ( empty( $user ) || empty( $user->ID ) ) {
519 return null;
520 }
521 $placeholders = array(
522 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
523 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
524 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
525 $user->data->user_login : null,
526 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
527 $user->data->display_name : null,
528 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
529 );
530 return $placeholders;
531 }
532
533 function get_ip_address( $params = null ) {
534 $ip = '127.0.0.1';
535 $headers = [
536 'HTTP_TRUE_CLIENT_IP',
537 'HTTP_CF_CONNECTING_IP',
538 'HTTP_X_REAL_IP',
539 'HTTP_CLIENT_IP',
540 'HTTP_X_FORWARDED_FOR',
541 'HTTP_X_FORWARDED',
542 'HTTP_X_CLUSTER_CLIENT_IP',
543 'HTTP_FORWARDED_FOR',
544 'HTTP_FORWARDED',
545 'REMOTE_ADDR',
546 ];
547
548 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
549 $ip = ( string )$params[ 'ip' ];
550 } else {
551 foreach ( $headers as $header ) {
552 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
553 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
554 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
555 break;
556 }
557 }
558 }
559
560 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
561 }
562
563 #endregion
564
565 #region Sanitization
566 function sanitize_sort( &$sort, $default_accessor = 'created', $default_order = 'DESC',
567 $allowed_columns = array( 'created', 'updated', 'name', 'id', 'time', 'units', 'price' )) {
568
569 // Ensure $sort is an array
570 if ( !is_array( $sort ) ) {
571 $sort = [ "accessor" => $default_accessor, "by" => $default_order ];
572 }
573 // Extract and sanitize the accessor
574 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
575 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
576 error_log( "AI Engine: This sort accessor is not allowed ($sort_accessor)." );
577 $sort_accessor = $default_accessor;
578 }
579 // Extract and sanitize the sort order
580 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
581 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
582 error_log( "AI Engine: This sort order is not allowed ($sort_by)." );
583 $sort_by = $default_order;
584 }
585 // Update the sort array with sanitized values
586 $sort['accessor'] = $sort_accessor;
587 $sort['by'] = $sort_by;
588 }
589 #endregion
590
591 #region Other Helpers
592 function safe_strlen( $string, $encoding = 'UTF-8' ) {
593 if ( function_exists( 'mb_strlen' ) ) {
594 return mb_strlen( $string, $encoding );
595 }
596 else {
597 // Fallback implementation for environments without mbstring extension
598 return preg_match_all( '/./u', $string, $matches );
599 }
600 }
601
602 public function check_rest_nonce( $request ) {
603 $nonce = $request->get_header( 'X-WP-Nonce' );
604 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
605 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
606 }
607
608 function get_random_id( $length = 8, $excludeIds = [] ) {
609 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
610 $charactersLength = strlen( $characters );
611 $randomId = '';
612 for ( $i = 0; $i < $length; $i++ ) {
613 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
614 }
615 if ( in_array( $randomId, $excludeIds ) ) {
616 return $this->get_random_id( $length, $excludeIds );
617 }
618 return $randomId;
619 }
620
621 function is_url( $url ) {
622 return strpos( $url, 'http' ) === 0 ? true : false;
623 }
624
625 function get_post_types() {
626 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
627 $post_types = array();
628 $types = get_post_types( [], 'objects' );
629
630 // Let's get the Post Types that are enabled for Embeddings Sync
631 $embeddingsSettings = $this->get_option( 'embeddings' );
632 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
633
634 foreach ( $types as $type ) {
635 $forced = in_array( $type->name, $syncPostTypes );
636 // Should not be excluded.
637 if ( !$forced && in_array( $type->name, $excluded ) ) {
638 continue;
639 }
640 // Should be public.
641 if ( !$forced && !$type->public ) {
642 continue;
643 }
644 $post_types[] = array(
645 'name' => $type->labels->name,
646 'type' => $type->name,
647 );
648 }
649
650 // Let's get the Post Types that are enabled for Embeddings Sync
651 $embeddingsSettings = $this->get_option( 'embeddings' );
652 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
653
654 return $post_types;
655 }
656
657 function get_post( $post ) {
658 if ( is_numeric( $post ) ) {
659 $post = get_post( $post );
660 }
661 if ( is_object( $post ) ) {
662 $post = (array)$post;
663 }
664 if ( !is_array( $post ) ) {
665 return null;
666 }
667 $language = $this->get_post_language( $post['ID'] );
668 $content = $this->get_post_content( $post['ID'] );
669 $title = $post['post_title'];
670 $excerpt = $post['post_excerpt'];
671 $url = get_permalink( $post['ID'] );
672 $checksum = wp_hash( $content . $title . $url );
673 return [
674 'postId' => (int)$post['ID'],
675 'title' => $title,
676 'content' => $content,
677 'excerpt' => $excerpt,
678 'url' => $url,
679 'language' => $language ?? 'english',
680 'checksum' => $checksum,
681 ];
682 }
683 #endregion
684
685 #region Usage & Costs
686
687 // Quick and dirty token estimation
688 // Let's keep this synchronized with Helpers in JS
689 static function estimate_tokens( ...$args ): int {
690 $text = "";
691 foreach ( $args as $arg ) {
692 if ( is_array( $arg ) ) {
693 foreach ( $arg as $message ) {
694 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
695 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
696 }
697 }
698 else if ( is_string( $arg ) ) {
699 $text .= $arg;
700 }
701 }
702 $averageTokenLength = 4;
703 $words = preg_split( '/\s+/', trim( $text ) );
704 $tokenCount = 0;
705 foreach ( $words as $word ) {
706 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
707 }
708 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
709 }
710
711 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
712 if ( !is_numeric( $in_tokens ) ) {
713 throw new Exception( 'AI Engine: in_tokens must be a number.' );
714 }
715 if ( !is_numeric( $out_tokens ) ) {
716 $out_tokens = 0;
717 }
718 if ( !$model ) {
719 throw new Exception( 'AI Engine: model is required.' );
720 }
721 $usage = $this->get_option( 'ai_models_usage' );
722 $month = date( 'Y-m' );
723 if ( !isset( $usage[$month] ) ) {
724 $usage[$month] = array();
725 }
726 if ( !isset( $usage[$month][$model] ) ) {
727 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
728 }
729 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
730 $usage[$month][$model]['completion_tokens'] += $out_tokens;
731 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
732 $this->update_option( 'ai_models_usage', $usage );
733 $usageInfo = [
734 'prompt_tokens' => $in_tokens,
735 'completion_tokens' => $out_tokens,
736 'total_tokens' => $in_tokens + $out_tokens,
737 ];
738 if ( $returned_price !== null ) {
739 $usageInfo['price'] = $returned_price;
740 }
741 return $usageInfo;
742 }
743
744 public function record_audio_usage( $model, $seconds ) {
745 if ( !is_numeric( $seconds ) ) {
746 throw new Exception( 'AI Engine: seconds must be a number.' );
747 }
748 if ( !$model ) {
749 throw new Exception( 'AI Engine: model is required.' );
750 }
751 $usage = $this->get_option( 'ai_models_usage' );
752 $month = date( 'Y-m' );
753 if ( !isset( $usage[$month] ) ) {
754 $usage[$month] = array();
755 }
756 if ( !isset( $usage[$month][$model] ) ) {
757 $usage[$month][$model] = array( 'seconds' => 0 );
758 }
759 $usage[$month][$model]['seconds'] += $seconds;
760 $this->update_option( 'ai_models_usage', $usage );
761 return [ 'seconds' => $seconds ];
762 }
763
764 public function record_images_usage( $model, $resolution, $images ) {
765 if ( !$model || !$resolution || !$images ) {
766 throw new Exception( 'Missing parameters for record_image_usage.' );
767 }
768 $usage = $this->get_option( 'ai_models_usage' );
769 $month = date( 'Y-m' );
770 if ( !isset( $usage[$month] ) ) {
771 $usage[$month] = array();
772 }
773 if ( !isset( $usage[$month][$model] ) ) {
774 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
775 }
776 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
777 $usage[$month][$model]['resolution'][$resolution] = 0;
778 }
779 $usage[$month][$model]['resolution'][$resolution] += $images;
780 $usage[$month][$model]['images'] += $images;
781 $this->update_option( 'ai_models_usage', $usage );
782 return [ 'resolution' => $resolution, 'images' => $images ];
783 }
784
785 #endregion
786
787 #region Streaming
788 public function stream_push( $data ) {
789 $data = apply_filters( 'mwai_stream_push', $data );
790 $out = "data: " . json_encode( $data );
791 echo $out;
792 echo "\n\n";
793 if ( ob_get_level() > 0 ) {
794 ob_end_flush();
795 }
796 flush();
797 }
798 #endregion
799
800 #region Options
801 function get_themes() {
802 $themes = get_option( $this->themes_option_name, [] );
803 $themes = empty( $themes ) ? [] : $themes;
804
805 $internalThemes = [
806 'chatgpt' => [
807 'type' => 'internal','name' => 'ChatGPT', 'themeId' => 'chatgpt',
808 'settings' => [], 'style' => ""
809 ],
810 'messages' => [
811 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
812 'settings' => [], 'style' => ""
813 ],
814 'timeless' => [
815 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
816 'settings' => [], 'style' => ""
817 ],
818 ];
819 $customThemes = [];
820 foreach ( $themes as $theme ) {
821 if ( isset( $internalThemes[$theme['themeId']] ) ) {
822 $internalThemes[$theme['themeId']] = $theme;
823 continue;
824 }
825 $customThemes[] = $theme;
826 }
827 return array_merge(array_values($internalThemes), $customThemes);
828 }
829
830 function update_themes( $themes ) {
831 update_option( $this->themes_option_name, $themes );
832 return $themes;
833 }
834
835 function get_chatbots() {
836 $chatbots = get_option( $this->chatbots_option_name, [] );
837 $hasChanges = false;
838 if ( empty( $chatbots ) ) {
839 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
840 }
841 $hasDefault = false;
842 foreach ( $chatbots as &$chatbot ) {
843 if ( $chatbot['botId'] === 'default' ) {
844 $hasDefault = true;
845 }
846 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
847 // Use default value if not set.
848 if ( !isset( $chatbot[$key] ) ) {
849 $chatbot[$key] = $value;
850 }
851 }
852 // TODO: After October 2024, let's remove this.
853 if ( isset( $chatbot['context'] ) ) {
854 $chatbot['instructions'] = $chatbot['context'];
855 unset( $chatbot['context'] );
856 $hasChanges = true;
857 }
858 // TODO: After October 2024, let's remove this.
859 if ( isset( $chatbot['fileUpload'] ) ) {
860 $chatbot['fileSearch'] = $chatbot['fileUpload'];
861 unset( $chatbot['fileUpload'] );
862 $hasChanges = true;
863 }
864 }
865 if ( !$hasDefault ) {
866 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
867 array_unshift( $chatbots, $defaultBot );
868 $hasChanges = true;
869 }
870 if ( $hasChanges ) {
871 update_option( $this->chatbots_option_name, $chatbots );
872 }
873 return $chatbots;
874 }
875
876 function get_chatbot( $botId ) {
877 $chatbots = $this->get_chatbots();
878 foreach ( $chatbots as $chatbot ) {
879 if ( $chatbot['botId'] === (string)$botId ) {
880 return $chatbot;
881 }
882 }
883 return null;
884 }
885
886 function get_embeddings_env( $envId ) {
887 $envs = $this->get_option( 'embeddings_envs' );
888 foreach ( $envs as $env ) {
889 if ( $env['id'] === $envId ) {
890 return $env;
891 }
892 }
893 return null;
894 }
895
896 function get_ai_env( $envId ) {
897 $envs = $this->get_option( 'ai_envs' );
898 foreach ( $envs as $env ) {
899 if ( $env['id'] === $envId ) {
900 return $env;
901 }
902 }
903 return null;
904 }
905
906 function get_assistant( $envId, $assistantId ) {
907 $env = $this->get_ai_env( $envId );
908 if ( !$env ) {
909 return null;
910 }
911 $assistants = $env['assistants'];
912 foreach ( $assistants as $assistant ) {
913 if ( $assistant['id'] === $assistantId ) {
914 return $assistant;
915 }
916 }
917 return null;
918 }
919
920 function get_theme( $themeId ) {
921 $themes = $this->get_themes();
922 foreach ( $themes as $theme ) {
923 if ( $theme['themeId'] === $themeId ) {
924 return $theme;
925 }
926 }
927 return null;
928 }
929
930 function update_chatbots( $chatbots ) {
931 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
932 // TODO: I think some HTML fields are missing, guestName, maybe others.
933 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
934 $keepLineReturnsFields = [ 'instructions' ];
935 $whiteSpacedFields = [ 'context' ];
936 foreach ( $chatbots as &$chatbot ) {
937 foreach ( $chatbot as $key => &$value ) {
938 if ( in_array( $key, $deprecatedFields ) ) {
939 unset( $chatbot[$key] );
940 continue;
941 }
942 if ( in_array( $key, $htmlFields ) ) {
943 $value = wp_kses_post( $value );
944 }
945 else if ( in_array( $key, $whiteSpacedFields ) ) {
946 $value = sanitize_textarea_field( $value );
947 }
948 else if ( $key === 'functions' ) {
949 $functions = [];
950 foreach ( $value as $function ) {
951 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
952 $functions[] = [
953 'id' => sanitize_text_field( $function['id'] ),
954 'type' => sanitize_text_field( $function['type'] ),
955 ];
956 }
957 }
958 $value = $functions;
959 }
960 else {
961 if ( in_array( $key, $keepLineReturnsFields ) ) {
962 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
963 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
964 }
965 $value = sanitize_text_field( $value );
966 if ( in_array( $key, $keepLineReturnsFields ) ) {
967 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
968 }
969 }
970 }
971 }
972 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
973 $this->log( '⚠️ Could not update chatbots.' );
974 $chatbots = get_option( $this->chatbots_option_name, [] );
975 return $chatbots;
976 }
977 return $chatbots;
978 }
979
980 function populate_dynamic_options( $options ) {
981 // Languages
982 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
983
984 // Consolidate the Engines and their Models
985 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
986 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
987 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
988 foreach ( $options['ai_engines'] as &$engine ) {
989 if ( $engine['type'] === 'openai' ) {
990 $engine['models'] = apply_filters( 'mwai_openai_models',
991 Meow_MWAI_Engines_OpenAI::get_models_static()
992 );
993 }
994 else if ( $engine['type'] === 'anthropic' ) {
995 $engine['models'] = apply_filters( 'mwai_anthropic_models',
996 Meow_MWAI_Engines_Anthropic::get_models_static()
997 );
998 }
999 else {
1000 $engine['models'] = [];
1001 foreach ( $options['ai_models'] as $model ) {
1002 if ( $model['type'] === $engine['type'] ) {
1003 $engine['models'][] = $model;
1004 }
1005 }
1006 }
1007 }
1008
1009 // Functions via Snippet Vault (or custom code)
1010 $json = [];
1011 $functions = apply_filters( 'mwai_functions_list', [] );
1012 foreach ( $functions as $function ) {
1013 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1014 }
1015 $options['functions'] = $json;
1016
1017 // Addons
1018 $options['addons'] = apply_filters( 'mwai_addons', [
1019 [
1020 'slug' => "mwai-notifications",
1021 'name' => "Notifications",
1022 'icon_url' => MeowCommon_Admin::$logo,
1023 'description' => "Add-on for AI Engine that adds notifications.",
1024 'install_url' => "https://meowapps.com/products/mwai-notifications/",
1025 'settings_url' => null,
1026 'enabled' => false,
1027 ], [
1028 'slug' => "mwai-ollama",
1029 'name' => "Ollama",
1030 'icon_url' => MeowCommon_Admin::$logo,
1031 'description' => "Support for local LLMs via Ollama. Select the 'Ollama' type in your 'Environments for AI', then you can 'Refresh Models' and use them!",
1032 'install_url' => "https://meowapps.com/products/mwai-ollama/",
1033 'settings_url' => null,
1034 'enabled' => false
1035 ]
1036 ] );
1037
1038 return $options;
1039 }
1040
1041 function get_all_options( $force = false, $sanitize = false ) {
1042 if ( $force || is_null( $this->options ) ) {
1043 $options = get_option( $this->option_name, [] );
1044 if ( $sanitize ) {
1045 $options = $this->sanitize_options( $options );
1046 }
1047 foreach ( MWAI_OPTIONS as $key => $value ) {
1048 if ( !isset( $options[$key] ) ) {
1049 $options[$key] = $value;
1050 }
1051 }
1052 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1053 $options['default_limits'] = MWAI_LIMITS;
1054 $this->options = $options;
1055 }
1056 $options = $this->populate_dynamic_options( $this->options );
1057 return $options;
1058 }
1059
1060 // Sanitize options when we update the plugin or perform some updates
1061 // if we change the structure of the options.
1062 function sanitize_options( $options ) {
1063 $needs_update = false;
1064
1065 // TODO: After October 2024, let's remove this.
1066 $old_options = [
1067 'openai_models',
1068 'anthropic_models',
1069 '${envType}_models',
1070 'shortcode_chat_params',
1071 'extra_models',
1072 'fallback_model',
1073 'mwai_advisor_data'
1074 ];
1075 foreach ( $old_options as $old_option ) {
1076 if ( isset( $options[$old_option] ) ) {
1077 unset( $options[$old_option] );
1078 $needs_update = true;
1079 }
1080 }
1081
1082 // Avoid the logs_path to be a PHP file.
1083 if ( isset( $options['logs_path'] ) ) {
1084 $logs_path = $options['logs_path'];
1085 if ( substr( $logs_path, -4 ) !== '.log' ) {
1086 $options['logs_path'] = '';
1087 $needs_update = true;
1088 }
1089 }
1090
1091 // TODO: After October 2024, let's remove this.
1092 #region Temporary Code
1093 if ( isset( $options['openrouter_models'] ) ) {
1094 foreach ( $options['openrouter_models'] as $model ) {
1095 $model['envId'] = null;
1096 $model['type'] = 'openrouter';
1097 $options['ai_models'][] = $model;
1098 }
1099 $needs_update = true;
1100 unset( $options['openrouter_models'] );
1101 }
1102 if ( isset( $options['google_models'] ) ) {
1103 foreach ( $options['google_models'] as $model ) {
1104 $model['envId'] = null;
1105 $model['type'] = 'google';
1106 $options['ai_models'][] = $model;
1107 }
1108 $needs_update = true;
1109 unset( $options['google_models'] );
1110 }
1111 if ( isset( $options['shortcode_chat_stream'] ) ) {
1112 $options['ai_streaming'] = $options['shortcode_chat_stream'];
1113 unset( $options['shortcode_chat_stream'] );
1114 $needs_update = true;
1115 }
1116 if ( isset( $options['shortcode_chat_syntax_highlighting'] ) ) {
1117 $options['syntax_highlight'] = $options['shortcode_chat_syntax_highlighting'];
1118 unset( $options['shortcode_chat_syntax_highlighting'] );
1119 $needs_update = true;
1120 }
1121 if ( isset( $options['shortcode_chat_moderation'] ) ) {
1122 $options['chatbot_moderation'] = $options['shortcode_chat_moderation'];
1123 unset( $options['shortcode_chat_moderation'] );
1124 $needs_update = true;
1125 }
1126 if ( isset( $options['shortcode_chat_discussions'] ) ) {
1127 $options['chatbot_discussions'] = $options['shortcode_chat_discussions'];
1128 unset( $options['shortcode_chat_discussions'] );
1129 $needs_update = true;
1130 }
1131 if ( isset( $options['shortcode_chat_typewriter'] ) ) {
1132 $options['chatbot_typewriter'] = $options['shortcode_chat_typewriter'];
1133 unset( $options['shortcode_chat_typewriter'] );
1134 $needs_update = true;
1135 }
1136 if ( isset( $options['shortcode_chat'] ) ) {
1137 $options['module_chatbots'] = $options['shortcode_chat'];
1138 unset( $options['shortcode_chat'] );
1139 $needs_update = true;
1140 }
1141 if ( isset( $options['openai_usage'] ) ) {
1142 $options['ai_models_usage'] = $options['openai_usage'];
1143 unset( $options['openai_usage'] );
1144 $needs_update = true;
1145 }
1146 #endregion
1147
1148 // The IDs for the embeddings environments are generated here.
1149 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1150 $embeddings_default_exists = false;
1151 if ( isset( $options['embeddings_envs'] ) ) {
1152 foreach ( $options['embeddings_envs'] as &$env ) {
1153 if ( !isset( $env['id'] ) ) {
1154 $env['id'] = $this->get_random_id();
1155 $needs_update = true;
1156 }
1157 if ( $env['id'] === $options['embeddings_default_env'] ) {
1158 $embeddings_default_exists = true;
1159 }
1160 }
1161 }
1162 if ( !$embeddings_default_exists ) {
1163 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1164 $needs_update = true;
1165 }
1166
1167 // The IDs for the AI environments are generated here.
1168 $allEnvIds = [];
1169 $ai_default_exists = false;
1170 if ( isset( $options['ai_envs'] ) ) {
1171 foreach ( $options['ai_envs'] as &$env ) {
1172 if ( !isset( $env['id'] ) ) {
1173 $env['id'] = $this->get_random_id();
1174 $needs_update = true;
1175 }
1176 if ( $env['id'] === $options['ai_default_env'] ) {
1177 $ai_default_exists = true;
1178 }
1179 $allEnvIds[] = $env['id'];
1180 }
1181 }
1182 if ( !$ai_default_exists ) {
1183 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1184 $needs_update = true;
1185 }
1186
1187 // All the models with an envId that does not exist anymore are removed.
1188 if ( isset( $options['ai_models'] ) ) {
1189 $options['ai_models'] = array_values( array_filter( $options['ai_models'],
1190 function( $model ) use ( $allEnvIds, &$needs_update ) {
1191 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1192 $needs_update = true;
1193 return false;
1194 }
1195 return true;
1196 }
1197 ) );
1198 }
1199
1200 if ( $needs_update ) {
1201 ksort( $options );
1202 update_option( $this->option_name, $options, false );
1203 }
1204
1205 return $options;
1206 }
1207
1208 function update_options( $options ) {
1209 if ( !update_option( $this->option_name, $options, false ) ) {
1210 return false;
1211 }
1212 $options = $this->get_all_options( true, true );
1213 return $options;
1214 }
1215
1216 function update_option( $option, $value ) {
1217 $options = $this->get_all_options( true );
1218 $options[$option] = $value;
1219 return $this->update_options( $options );
1220 }
1221
1222 function get_option( $option, $default = null ) {
1223 $options = $this->get_all_options();
1224 return $options[$option] ?? $default;
1225 }
1226
1227 function update_ai_env( $env_id, $option, $value ) {
1228 $options = $this->get_all_options( true );
1229 foreach ( $options['ai_envs'] as &$env ) {
1230 if ( $env['id'] === $env_id ) {
1231 $env[$option] = $value;
1232 break;
1233 }
1234 }
1235 return $this->update_options( $options );
1236 }
1237
1238 function get_engine_models( $engineType ) {
1239 $engines = $this->get_option( 'ai_engines' );
1240 foreach ( $engines as $engine ) {
1241 if ( $engine['type'] === $engineType ) {
1242 return isset( $engine['models'] ) ? $engine['models'] : [];
1243 }
1244 }
1245 return [];
1246 }
1247
1248 function reset_options() {
1249 delete_option( $this->themes_option_name );
1250 delete_option( $this->chatbots_option_name );
1251 delete_option( $this->option_name );
1252 return $this->get_all_options( true );
1253 }
1254 #endregion
1255
1256 #region Logs
1257
1258 function get_logs() {
1259 $log_file_path = $this->get_logs_path();
1260
1261 if ( !file_exists( $log_file_path ) ) {
1262 return "Empty log file.";
1263 }
1264
1265 $content = file_get_contents( $log_file_path );
1266 $lines = explode( "\n", $content );
1267 $lines = array_filter( $lines );
1268 $lines = array_reverse( $lines );
1269 $content = implode( "\n", $lines );
1270 return $content;
1271 }
1272
1273 function clear_logs() {
1274 $logPath = $this->get_logs_path();
1275 if ( file_exists( $logPath ) ) {
1276 unlink( $logPath );
1277 }
1278
1279 $options = $this->get_all_options();
1280 $options['logs_path'] = null;
1281 $this->update_options( $options );
1282 }
1283
1284 function get_logs_path() {
1285 $uploads_dir = wp_upload_dir();
1286 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
1287
1288 $path = $this->get_option( 'logs_path' );
1289
1290 if ( $path && file_exists( $path ) ) {
1291 // make sure the path is legal (within the uploads directory with the MWAI_PREFIX and log extension)
1292 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWAI_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
1293 $path = null;
1294 } else {
1295 return $path;
1296 }
1297 }
1298
1299 if ( !$path ) {
1300 $path = $uploads_dir_path . MWAI_PREFIX . "_" . $this->random_ascii_chars() . ".log";
1301 if ( !file_exists( $path ) ) {
1302 touch( $path );
1303 }
1304 $options = $this->get_all_options();
1305 $options['logs_path'] = $path;
1306 $this->update_options( $options );
1307 }
1308
1309 return $path;
1310 }
1311
1312 function log( $data = null ) {
1313 if ( !$this->get_option( 'module_devtools', false ) ) {
1314 return false;
1315 }
1316 if ( !$this->get_option( 'server_debug_mode', false ) ) {
1317 return false;
1318 }
1319 $log_file_path = $this->get_logs_path();
1320 $fh = @fopen( $log_file_path, 'a' );
1321 if ( !$fh ) { return false; }
1322 $date = date( "Y-m-d H:i:s" );
1323 if ( is_null( $data ) ) {
1324 fwrite( $fh, "\n" );
1325 }
1326 else {
1327 fwrite( $fh, "$date: {$data}\n" );
1328 //error_log( "[MWAI] $data" );
1329 }
1330 fclose( $fh );
1331 return true;
1332 }
1333
1334 private function random_ascii_chars( $length = 8 ) {
1335 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
1336 $characters_length = count( $characters );
1337 $random_string = '';
1338
1339 for ( $i = 0; $i < $length; $i++ ) {
1340 $random_string .= $characters[rand(0, $characters_length - 1)];
1341 }
1342
1343 return $random_string;
1344 }
1345
1346 #endregion
1347 }
1348
1349 ?>