PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.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 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
1366 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 'slug' => "mwai-websearch",
1037 'name' => "Web Search",
1038 'icon_url' => MeowCommon_Admin::$logo,
1039 'description' => "This add-on for AI Engine enhances the AI models' responses by incorporating additional context. It currently supports Google Search.",
1040 'install_url' => "https://meowapps.com/products/mwai-websearch/",
1041 'settings_url' => null,
1042 'enabled' => false
1043 ], [
1044 'slug' => "mwai-better-links",
1045 'name' => "Better Links",
1046 'icon_url' => MeowCommon_Admin::$logo,
1047 'description' => "This add-on for AI Engine adds link validation and term linking features.",
1048 'install_url' => "https://meowapps.com/products/mwai-better-links/",
1049 'settings_url' => null,
1050 'enabled' => false
1051 ]
1052 ] );
1053
1054 return $options;
1055 }
1056
1057 function get_all_options( $force = false, $sanitize = false ) {
1058 if ( $force || is_null( $this->options ) ) {
1059 $options = get_option( $this->option_name, [] );
1060 $init_mode = empty( $options );
1061 foreach ( MWAI_OPTIONS as $key => $value ) {
1062 if ( !isset( $options[$key] ) ) {
1063 $options[$key] = $value;
1064 }
1065 }
1066 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1067 $options['default_limits'] = MWAI_LIMITS;
1068 if ( $sanitize || $init_mode ) {
1069 $options = $this->sanitize_options( $options );
1070 }
1071 $this->options = $options;
1072 }
1073 $options = $this->populate_dynamic_options( $this->options );
1074 return $options;
1075 }
1076
1077 // Sanitize options when we update the plugin or perform some updates
1078 // if we change the structure of the options.
1079 function sanitize_options( $options ) {
1080 $needs_update = false;
1081
1082 // TODO: After October 2024, let's remove this.
1083 $old_options = [
1084 'openai_models',
1085 'anthropic_models',
1086 '${envType}_models',
1087 'shortcode_chat_params',
1088 'extra_models',
1089 'fallback_model',
1090 'mwai_advisor_data'
1091 ];
1092 foreach ( $old_options as $old_option ) {
1093 if ( isset( $options[$old_option] ) ) {
1094 unset( $options[$old_option] );
1095 $needs_update = true;
1096 }
1097 }
1098
1099 // Avoid the logs_path to be a PHP file.
1100 if ( isset( $options['logs_path'] ) ) {
1101 $logs_path = $options['logs_path'];
1102 if ( substr( $logs_path, -4 ) !== '.log' ) {
1103 $options['logs_path'] = '';
1104 $needs_update = true;
1105 }
1106 }
1107
1108 // TODO: After October 2024, let's remove this.
1109 #region Temporary Code
1110 if ( isset( $options['openrouter_models'] ) ) {
1111 foreach ( $options['openrouter_models'] as $model ) {
1112 $model['envId'] = null;
1113 $model['type'] = 'openrouter';
1114 $options['ai_models'][] = $model;
1115 }
1116 $needs_update = true;
1117 unset( $options['openrouter_models'] );
1118 }
1119 if ( isset( $options['google_models'] ) ) {
1120 foreach ( $options['google_models'] as $model ) {
1121 $model['envId'] = null;
1122 $model['type'] = 'google';
1123 $options['ai_models'][] = $model;
1124 }
1125 $needs_update = true;
1126 unset( $options['google_models'] );
1127 }
1128 if ( isset( $options['shortcode_chat_stream'] ) ) {
1129 $options['ai_streaming'] = $options['shortcode_chat_stream'];
1130 unset( $options['shortcode_chat_stream'] );
1131 $needs_update = true;
1132 }
1133 if ( isset( $options['shortcode_chat_syntax_highlighting'] ) ) {
1134 $options['syntax_highlight'] = $options['shortcode_chat_syntax_highlighting'];
1135 unset( $options['shortcode_chat_syntax_highlighting'] );
1136 $needs_update = true;
1137 }
1138 if ( isset( $options['shortcode_chat_moderation'] ) ) {
1139 $options['chatbot_moderation'] = $options['shortcode_chat_moderation'];
1140 unset( $options['shortcode_chat_moderation'] );
1141 $needs_update = true;
1142 }
1143 if ( isset( $options['shortcode_chat_discussions'] ) ) {
1144 $options['chatbot_discussions'] = $options['shortcode_chat_discussions'];
1145 unset( $options['shortcode_chat_discussions'] );
1146 $needs_update = true;
1147 }
1148 if ( isset( $options['shortcode_chat_typewriter'] ) ) {
1149 $options['chatbot_typewriter'] = $options['shortcode_chat_typewriter'];
1150 unset( $options['shortcode_chat_typewriter'] );
1151 $needs_update = true;
1152 }
1153 if ( isset( $options['shortcode_chat'] ) ) {
1154 $options['module_chatbots'] = $options['shortcode_chat'];
1155 unset( $options['shortcode_chat'] );
1156 $needs_update = true;
1157 }
1158 if ( isset( $options['openai_usage'] ) ) {
1159 $options['ai_models_usage'] = $options['openai_usage'];
1160 unset( $options['openai_usage'] );
1161 $needs_update = true;
1162 }
1163 #endregion
1164
1165 // The IDs for the embeddings environments are generated here.
1166 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1167 $embeddings_default_exists = false;
1168 if ( isset( $options['embeddings_envs'] ) ) {
1169 foreach ( $options['embeddings_envs'] as &$env ) {
1170 if ( !isset( $env['id'] ) ) {
1171 $env['id'] = $this->get_random_id();
1172 $needs_update = true;
1173 }
1174 if ( $env['id'] === $options['embeddings_default_env'] ) {
1175 $embeddings_default_exists = true;
1176 }
1177 }
1178 }
1179 if ( !$embeddings_default_exists ) {
1180 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1181 $needs_update = true;
1182 }
1183
1184 // The IDs for the AI environments are generated here.
1185 $allEnvIds = [];
1186 $ai_default_exists = false;
1187 if ( isset( $options['ai_envs'] ) ) {
1188 foreach ( $options['ai_envs'] as &$env ) {
1189 if ( !isset( $env['id'] ) ) {
1190 $env['id'] = $this->get_random_id();
1191 $needs_update = true;
1192 }
1193 if ( $env['id'] === $options['ai_default_env'] ) {
1194 $ai_default_exists = true;
1195 }
1196 $allEnvIds[] = $env['id'];
1197 }
1198 }
1199 if ( !$ai_default_exists ) {
1200 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1201 $needs_update = true;
1202 }
1203
1204 // All the models with an envId that does not exist anymore are removed.
1205 if ( isset( $options['ai_models'] ) ) {
1206 $options['ai_models'] = array_values( array_filter( $options['ai_models'],
1207 function( $model ) use ( $allEnvIds, &$needs_update ) {
1208 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1209 $needs_update = true;
1210 return false;
1211 }
1212 return true;
1213 }
1214 ) );
1215 }
1216
1217 if ( $needs_update ) {
1218 ksort( $options );
1219 update_option( $this->option_name, $options, false );
1220 }
1221
1222 return $options;
1223 }
1224
1225 function update_options( $options ) {
1226 if ( !update_option( $this->option_name, $options, false ) ) {
1227 return false;
1228 }
1229 $options = $this->get_all_options( true, true );
1230 return $options;
1231 }
1232
1233 function update_option( $option, $value ) {
1234 $options = $this->get_all_options( true );
1235 $options[$option] = $value;
1236 return $this->update_options( $options );
1237 }
1238
1239 function get_option( $option, $default = null ) {
1240 $options = $this->get_all_options();
1241 return $options[$option] ?? $default;
1242 }
1243
1244 function update_ai_env( $env_id, $option, $value ) {
1245 $options = $this->get_all_options( true );
1246 foreach ( $options['ai_envs'] as &$env ) {
1247 if ( $env['id'] === $env_id ) {
1248 $env[$option] = $value;
1249 break;
1250 }
1251 }
1252 return $this->update_options( $options );
1253 }
1254
1255 function get_engine_models( $engineType ) {
1256 $engines = $this->get_option( 'ai_engines' );
1257 foreach ( $engines as $engine ) {
1258 if ( $engine['type'] === $engineType ) {
1259 return isset( $engine['models'] ) ? $engine['models'] : [];
1260 }
1261 }
1262 return [];
1263 }
1264
1265 function reset_options() {
1266 delete_option( $this->themes_option_name );
1267 delete_option( $this->chatbots_option_name );
1268 delete_option( $this->option_name );
1269 return $this->get_all_options( true );
1270 }
1271 #endregion
1272
1273 #region Logs
1274
1275 function get_logs() {
1276 $log_file_path = $this->get_logs_path();
1277
1278 if ( !file_exists( $log_file_path ) ) {
1279 return "Empty log file.";
1280 }
1281
1282 $content = file_get_contents( $log_file_path );
1283 $lines = explode( "\n", $content );
1284 $lines = array_filter( $lines );
1285 $lines = array_reverse( $lines );
1286 $content = implode( "\n", $lines );
1287 return $content;
1288 }
1289
1290 function clear_logs() {
1291 $logPath = $this->get_logs_path();
1292 if ( file_exists( $logPath ) ) {
1293 unlink( $logPath );
1294 }
1295
1296 $options = $this->get_all_options();
1297 $options['logs_path'] = null;
1298 $this->update_options( $options );
1299 }
1300
1301 function get_logs_path() {
1302 $uploads_dir = wp_upload_dir();
1303 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
1304
1305 $path = $this->get_option( 'logs_path' );
1306
1307 if ( $path && file_exists( $path ) ) {
1308 // make sure the path is legal (within the uploads directory with the MWAI_PREFIX and log extension)
1309 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWAI_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
1310 $path = null;
1311 } else {
1312 return $path;
1313 }
1314 }
1315
1316 if ( !$path ) {
1317 $path = $uploads_dir_path . MWAI_PREFIX . "_" . $this->random_ascii_chars() . ".log";
1318 if ( !file_exists( $path ) ) {
1319 touch( $path );
1320 }
1321 $options = $this->get_all_options();
1322 $options['logs_path'] = $path;
1323 $this->update_options( $options );
1324 }
1325
1326 return $path;
1327 }
1328
1329 function log( $data = null ) {
1330 if ( !$this->get_option( 'module_devtools', false ) ) {
1331 return false;
1332 }
1333 if ( !$this->get_option( 'server_debug_mode', false ) ) {
1334 return false;
1335 }
1336 $log_file_path = $this->get_logs_path();
1337 $fh = @fopen( $log_file_path, 'a' );
1338 if ( !$fh ) { return false; }
1339 $date = date( "Y-m-d H:i:s" );
1340 if ( is_null( $data ) ) {
1341 fwrite( $fh, "\n" );
1342 }
1343 else {
1344 fwrite( $fh, "$date: {$data}\n" );
1345 //error_log( "[MWAI] $data" );
1346 }
1347 fclose( $fh );
1348 return true;
1349 }
1350
1351 private function random_ascii_chars( $length = 8 ) {
1352 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
1353 $characters_length = count( $characters );
1354 $random_string = '';
1355
1356 for ( $i = 0; $i < $length; $i++ ) {
1357 $random_string .= $characters[rand(0, $characters_length - 1)];
1358 }
1359
1360 return $random_string;
1361 }
1362
1363 #endregion
1364 }
1365
1366 ?>