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