PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.2.57
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.2.57
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / core.php
ai-engine / classes Last commit date
engines 2 years ago modules 2 years ago queries 2 years ago admin.php 2 years ago api.php 2 years ago core.php 2 years ago init.php 3 years ago reply.php 2 years ago rest.php 2 years ago
core.php
1005 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 private $option_name = 'mwai_options';
21 private $themes_option_name = 'mwai_themes';
22 private $chatbots_option_name = 'mwai_chatbots';
23 private $nonce = null;
24
25 public $chatbot = null;
26 public $discussions = null;
27
28 public function __construct() {
29 $this->site_url = get_site_url();
30 $this->is_rest = MeowCommon_Helpers::is_rest();
31 $this->is_cli = defined( 'WP_CLI' );
32 $this->files = new Meow_MWAI_Modules_Files( $this );
33 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
34
35 add_action( 'plugins_loaded', array( $this, 'init' ) );
36 add_action( 'wp_register_script', array( $this, 'register_scripts' ) );
37 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
38 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
39 }
40
41 #region Init & Scripts
42 function init() {
43 global $mwai;
44 $this->chatbot = null;
45 $this->discussions = null;
46 new Meow_MWAI_Modules_Security( $this );
47 if ( $this->is_rest ) {
48 new Meow_MWAI_Rest( $this );
49 }
50 if ( is_admin() ) {
51 new Meow_MWAI_Admin( $this );
52 new Meow_MWAI_Modules_Utilities( $this );
53 }
54 if ( $this->get_option( 'shortcode_chat' ) ) {
55 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
56 $this->discussions = new Meow_MWAI_Modules_Discussions();
57 }
58
59 // Advanced core
60 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
61 new MeowPro_MWAI_Core( $this );
62 }
63
64 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
65 }
66
67 public function register_scripts() {
68 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
69 }
70
71 public function enqueue_scripts() {
72 $this->register_scripts();
73 wp_enqueue_script( "mwai_highlight" );
74 }
75
76 #endregion
77
78 #region Roles & Capabilities
79
80 function can_access_settings() {
81 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
82 }
83
84 function can_access_features() {
85 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
86 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
87 }
88
89 function can_access_public_api( $feature, $extra ) {
90 $logged_in = is_user_logged_in();
91 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
92 }
93
94 #endregion
95
96 #region AI-Related Helpers
97 function run_query( $query, $streamCallback = null, $markdown = false ) {
98 $envId = !empty( $query->envId ) ? $query->envId : null;
99 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
100
101 // If the engine is not set, we need to set it to the default one.
102 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
103 if ( $query instanceof Meow_MWAI_Query_Text ) {
104 $this->set_if_empty_defaults( $query, 'ai_default_env', 'ai_default_model' );
105 }
106 if ( $query instanceof Meow_MWAI_Query_Embed ) {
107 $this->set_if_empty_defaults( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
108 }
109 else if ( $query instanceof Meow_MWAI_Query_Image ) {
110 $this->set_if_empty_defaults( $query, 'ai_images_default_env', 'ai_images_default_model' );
111 }
112 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
113 $this->set_if_empty_defaults( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
114 }
115 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
116 }
117
118 // Let's run the query.
119 $reply = $engine->run( $query, $streamCallback );
120
121 // Let's allow to modify the reply before it is sent.
122 if ( $markdown ) {
123 if ( $query instanceof Meow_MWAI_Query_Image ) {
124 $reply->result = "";
125 foreach ( $reply->results as $result ) {
126 $reply->result .= "![Image]($result)\n";
127 }
128 }
129 }
130
131 return $reply;
132 }
133
134 private function set_if_empty_defaults( $query, $envOption, $modelOption ) {
135 $defaultEnv = $this->get_option( $envOption );
136 $defaultModel = $this->get_option( $modelOption );
137 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
138 throw new Exception( 'AI Engine: The default environment and model are not set.' );
139 }
140 $query->set_env_id( $defaultEnv );
141 $query->set_model( $defaultModel );
142 }
143 #endregion
144
145 #region Text-Related Helpers
146
147 // Clean the text perfectly, resolve shortcodes, etc, etc.
148 function clean_text( $rawText = "" ) {
149 $text = html_entity_decode( $rawText );
150 $text = wp_strip_all_tags( $text );
151 $text = preg_replace( '/[\r\n]+/', "\n", $text );
152 $text = preg_replace( '/\n+/', "\n", $text );
153 $text = preg_replace( '/\t+/', "\t", $text );
154 return $text . " ";
155 }
156
157 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
158 function clean_sentences( $text, $maxLength = null ) {
159 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
160 $sentences = preg_split('/(?<=[.?!。.!?])+/u', $text);
161 $hashes = array();
162 $uniqueSentences = array();
163 $total = 0;
164 foreach ( $sentences as $sentence ) {
165 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
166 $hash = md5( $sentence );
167 if ( !in_array( $hash, $hashes ) ) {
168 $length = strlen( $sentence );
169 if ( $total + $length > $maxLength ) {
170 continue;
171 }
172 $hashes[] = $hash;
173 $uniqueSentences[] = $sentence;
174 $total += $length;
175 }
176 }
177 $freshText = implode( " ", $uniqueSentences );
178 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
179 return $freshText;
180 }
181
182 function get_post_content( $postId ) {
183 $post = get_post( $postId );
184 if ( !$post ) {
185 return false;
186 }
187 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
188 $pattern = '/\[mwai_.*?\]/';
189 $text = preg_replace( $pattern, '', $text );
190 if ( $this->get_option( 'resolve_shortcodes' ) ) {
191 $text = apply_filters( 'the_content', $text );
192 }
193 else {
194 $pattern = "/\[[^\]]+\]/";
195 $text = preg_replace( $pattern, '', $text );
196 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
197 $text = preg_replace( $pattern, '', $text );
198 }
199 $text = $this->clean_text( $text );
200 $text = $this->clean_sentences( $text );
201 $text = apply_filters( 'mwai_post_content', $text, $postId );
202 return $text;
203 }
204
205 function markdown_to_html( $content ) {
206 $Parsedown = new Parsedown();
207 $content = $Parsedown->text( $content );
208 return $content;
209 }
210
211 function get_post_language( $postId ) {
212 $locale = get_locale();
213 $code = strtolower( substr( $locale, 0, 2 ) );
214 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
215 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
216 if ( !empty( $lang ) ) {
217 $locale = $lang['locale'];
218 $humanLanguage = $lang['display_name'];
219 }
220 return strtolower( "$locale ($humanLanguage)" );
221 }
222 #endregion
223
224 #region Image-Related Helpers
225 function download_image( $url ) {
226 $args = array( 'timeout' => 60, );
227 $response = wp_safe_remote_get( $url, $args );
228 if ( is_wp_error( $response ) ) {
229 throw new Exception( $response->get_error_message() );
230 }
231 $output = wp_remote_retrieve_body( $response );
232 if ( is_wp_error( $output ) ) {
233 throw new Exception( $output->get_error_message() );
234 }
235 return $output;
236 }
237
238 /**
239 * Add an image from a URL to the Media Library.
240 * @param string $url The URL of the image to be downloaded.
241 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
242 * @param string $title The title of the image.
243 * @param string $description The description of the image.
244 * @param string $caption The caption of the image.
245 * @param string $alt The alt text of the image.
246 * @return int The attachment ID of the image.
247 */
248 public function add_image_from_url( $url, $filename = null, $title = null,
249 $description = null, $caption = null, $alt = null ) {
250 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
251 $url_filename = $path_parts['basename'];
252 $file_type = wp_check_filetype( $url_filename, null );
253 $allowed_types = get_allowed_mime_types();
254 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
255 throw new Exception( 'Invalid file type.' );
256 }
257 $extension = $file_type['ext'];
258 $image_data = $this->download_image( $url );
259 if ( !$image_data ) {
260 throw new Exception( 'Could not download the image.' );
261 }
262 $upload_dir = wp_upload_dir();
263
264 // If filename is not set or starts with 'generated_', we will generate a new filename.
265 if ( empty( $filename ) ) {
266 $filename = sanitize_file_name( $url_filename );
267 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
268 if ( empty( $extension ) ) {
269 $extension = $file_type['ext'];
270 }
271 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
272 $filename = $this->get_random_id( 16 ) . '.' . $extension;
273 }
274 if ( strpos( $filename, '.' ) === false ) {
275 $filename .= '.' . $extension;
276 }
277 }
278 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
279 $file = $upload_dir['path'] . '/' . $filename;
280 }
281 else {
282 $file = $upload_dir['basedir'] . '/' . $filename;
283 }
284
285 // Make sure the file is unique, if not, add a number to the end of the file before the extension
286 $i = 1;
287 $parts = pathinfo( $file );
288 while ( file_exists( $file ) ) {
289 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
290 $i++;
291 }
292
293 // Write the file
294 file_put_contents( $file, $image_data );
295 $attachment = [
296 'post_mime_type' => $file_type['type'],
297 'post_title' => $title ?? '',
298 'post_content' => $description ?? '',
299 'post_excerpt' => $caption ?? '',
300 'post_status' => 'inherit'
301 ];
302 // Register the file as a Media Library attachment
303 $attachmentId = wp_insert_attachment( $attachment, $file );
304 require_once( ABSPATH . 'wp-admin/includes/image.php' );
305 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
306 wp_update_attachment_metadata( $attachmentId, $attachment_data );
307 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
308 return $attachmentId;
309 }
310 #endregion
311
312 #region Context-Related Helpers
313 function retrieve_context( $params, $query ) {
314 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
315 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
316 $context = apply_filters( 'mwai_context_search', [], $query, [
317 'embeddingsEnvId' => $embeddingsEnvId
318 ]);
319 if ( empty( $context ) ) {
320 return null;
321 }
322 else if ( !isset( $context['content'] ) ) {
323 error_log( "AI Engine: A context without content was returned." );
324 return null;
325 }
326 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
327 $context['length'] = strlen( $context['content'] );
328 return $context;
329 }
330 #endregion
331
332 #region Users/Sessions Helpers
333
334 function get_nonce() {
335 // if ( !is_user_logged_in() ) {
336 // return null;
337 // }
338 if ( isset( $this->nonce ) ) {
339 return $this->nonce;
340 }
341 $this->nonce = wp_create_nonce( 'wp_rest' );
342 return $this->nonce;
343 }
344
345 function get_session_id() {
346 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
347 return $_COOKIE['mwai_session_id'];
348 }
349 return "N/A";
350 }
351
352 // Get the UserID from the data, or from the current user
353 function get_user_id( $data = null ) {
354 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
355 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
356 $admin = get_users( [ 'role' => 'administrator' ] );
357 if ( !empty( $admin ) ) {
358 return $admin[0]->ID;
359 }
360 }
361 if ( isset( $data ) && isset( $data['userId'] ) ) {
362 return (int)$data['userId'];
363 }
364 if ( is_user_logged_in() ) {
365 $current_user = wp_get_current_user();
366 if ( $current_user->ID > 0 ) {
367 return $current_user->ID;
368 }
369 }
370 return null;
371 }
372
373 function get_admin_user() {
374 $admin = get_users( [ 'role' => 'administrator' ] );
375 if ( !empty( $admin ) ) {
376 return $admin[0];
377 }
378 return null;
379 }
380
381 function get_user_data() {
382 $user = wp_get_current_user();
383 if ( empty( $user ) || empty( $user->ID ) ) {
384 return null;
385 }
386 $placeholders = array(
387 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
388 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
389 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
390 $user->data->user_login : null,
391 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
392 $user->data->display_name : null,
393 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
394 );
395 return $placeholders;
396 }
397
398 function get_ip_address( $params = null ) {
399 $ip = '127.0.0.1';
400 $headers = [
401 'HTTP_TRUE_CLIENT_IP',
402 'HTTP_CF_CONNECTING_IP',
403 'HTTP_X_REAL_IP',
404 'HTTP_CLIENT_IP',
405 'HTTP_X_FORWARDED_FOR',
406 'HTTP_X_FORWARDED',
407 'HTTP_X_CLUSTER_CLIENT_IP',
408 'HTTP_FORWARDED_FOR',
409 'HTTP_FORWARDED',
410 'REMOTE_ADDR',
411 ];
412
413 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
414 $ip = ( string )$params[ 'ip' ];
415 } else {
416 foreach ( $headers as $header ) {
417 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
418 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
419 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
420 break;
421 }
422 }
423 }
424
425 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
426 }
427
428 #endregion
429
430 #region Other Helpers
431
432 public function check_rest_nonce( $request ) {
433 $nonce = $request->get_header( 'X-WP-Nonce' );
434 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
435 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
436 }
437
438 function get_random_id( $length = 8, $excludeIds = [] ) {
439 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
440 $charactersLength = strlen( $characters );
441 $randomId = '';
442 for ( $i = 0; $i < $length; $i++ ) {
443 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
444 }
445 if ( in_array( $randomId, $excludeIds ) ) {
446 return $this->get_random_id( $length, $excludeIds );
447 }
448 return $randomId;
449 }
450
451 function is_url( $url ) {
452 return strpos( $url, 'http' ) === 0 ? true : false;
453 }
454
455 function get_post_types() {
456 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
457 $post_types = array();
458 $types = get_post_types( [], 'objects' );
459
460 // Let's get the Post Types that are enabled for Embeddings Sync
461 $embeddingsSettings = $this->get_option( 'embeddings' );
462 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
463
464 foreach ( $types as $type ) {
465 $forced = in_array( $type->name, $syncPostTypes );
466 // Should not be excluded.
467 if ( !$forced && in_array( $type->name, $excluded ) ) {
468 continue;
469 }
470 // Should be public.
471 if ( !$forced && !$type->public ) {
472 continue;
473 }
474 $post_types[] = array(
475 'name' => $type->labels->name,
476 'type' => $type->name,
477 );
478 }
479
480 // Let's get the Post Types that are enabled for Embeddings Sync
481 $embeddingsSettings = $this->get_option( 'embeddings' );
482 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
483
484 return $post_types;
485 }
486
487 function get_post( $post ) {
488 if ( is_numeric( $post ) ) {
489 $post = get_post( $post );
490 }
491 if ( is_object( $post ) ) {
492 $post = (array)$post;
493 }
494 if ( !is_array( $post ) ) {
495 return null;
496 }
497 $language = $this->get_post_language( $post['ID'] );
498 $content = $this->get_post_content( $post['ID'] );
499 $title = $post['post_title'];
500 $excerpt = $post['post_excerpt'];
501 $url = get_permalink( $post['ID'] );
502 $checksum = wp_hash( $content . $title . $url );
503 return [
504 'postId' => (int)$post['ID'],
505 'title' => $title,
506 'content' => $content,
507 'excerpt' => $excerpt,
508 'url' => $url,
509 'language' => $language ?? 'english',
510 'checksum' => $checksum,
511 ];
512 }
513 #endregion
514
515 #region Usage & Costs
516
517 // Quick and dirty token estimation
518 // Let's keep this synchronized with Helpers in JS
519 static function estimate_tokens( ...$args ): int {
520 $text = "";
521 foreach ( $args as $arg ) {
522 if ( is_array( $arg ) ) {
523 foreach ( $arg as $message ) {
524 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
525 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
526 }
527 }
528 else if ( is_string( $arg ) ) {
529 $text .= $arg;
530 }
531 }
532 $averageTokenLength = 4;
533 $words = preg_split( '/\s+/', trim( $text ) );
534 $tokenCount = 0;
535 foreach ( $words as $word ) {
536 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
537 }
538 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
539 }
540
541 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0 ) {
542 if ( !is_numeric( $in_tokens ) ) {
543 throw new Exception( 'AI Engine: in_tokens must be a number.' );
544 }
545 if ( !is_numeric( $out_tokens ) ) {
546 $out_tokens = 0;
547 }
548 if ( !$model ) {
549 throw new Exception( 'AI Engine: model is required.' );
550 }
551 $usage = $this->get_option( 'openai_usage' );
552 $month = date( 'Y-m' );
553 if ( !isset( $usage[$month] ) ) {
554 $usage[$month] = array();
555 }
556 if ( !isset( $usage[$month][$model] ) ) {
557 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
558 }
559 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
560 $usage[$month][$model]['completion_tokens'] += $out_tokens;
561 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
562 $this->update_option( 'openai_usage', $usage );
563 return [
564 'prompt_tokens' => $in_tokens,
565 'completion_tokens' => $out_tokens,
566 'total_tokens' => $in_tokens + $out_tokens
567 ];
568 }
569
570 public function record_audio_usage( $model, $seconds ) {
571 if ( !is_numeric( $seconds ) ) {
572 throw new Exception( 'AI Engine: seconds must be a number.' );
573 }
574 if ( !$model ) {
575 throw new Exception( 'AI Engine: model is required.' );
576 }
577 $usage = $this->get_option( 'openai_usage' );
578 $month = date( 'Y-m' );
579 if ( !isset( $usage[$month] ) ) {
580 $usage[$month] = array();
581 }
582 if ( !isset( $usage[$month][$model] ) ) {
583 $usage[$month][$model] = array( 'seconds' => 0 );
584 }
585 $usage[$month][$model]['seconds'] += $seconds;
586 $this->update_option( 'openai_usage', $usage );
587 return [ 'seconds' => $seconds ];
588 }
589
590 public function record_images_usage( $model, $resolution, $images ) {
591 if ( !$model || !$resolution || !$images ) {
592 throw new Exception( 'Missing parameters for record_image_usage.' );
593 }
594 $usage = $this->get_option( 'openai_usage' );
595 $month = date( 'Y-m' );
596 if ( !isset( $usage[$month] ) ) {
597 $usage[$month] = array();
598 }
599 if ( !isset( $usage[$month][$model] ) ) {
600 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
601 }
602 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
603 $usage[$month][$model]['resolution'][$resolution] = 0;
604 }
605 $usage[$month][$model]['resolution'][$resolution] += $images;
606 $usage[$month][$model]['images'] += $images;
607 $this->update_option( 'openai_usage', $usage );
608 return [ 'resolution' => $resolution, 'images' => $images ];
609 }
610
611 #endregion
612
613 #region Streaming
614 public function stream_push( $data ) {
615 $out = "data: " . json_encode( $data );
616 echo $out;
617 echo "\n\n";
618 if ( ob_get_level() > 0 ) {
619 ob_end_flush();
620 }
621 flush();
622 }
623 #endregion
624
625 #region Options
626 function get_themes() {
627 $themes = get_option( $this->themes_option_name, [] );
628 $themes = empty( $themes ) ? [] : $themes;
629
630 $internalThemes = [
631 'chatgpt' => [
632 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
633 'settings' => [], 'style' => ""
634 ],
635 'messages' => [
636 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
637 'settings' => [], 'style' => ""
638 ],
639 ];
640 $customThemes = [];
641 foreach ( $themes as $theme ) {
642 if ( isset( $internalThemes[$theme['themeId']] ) ) {
643 $internalThemes[$theme['themeId']] = $theme;
644 continue;
645 }
646 $customThemes[] = $theme;
647 }
648 return array_merge(array_values($internalThemes), $customThemes);
649 }
650
651 function update_themes( $themes ) {
652 update_option( $this->themes_option_name, $themes );
653 return $themes;
654 }
655
656 function get_chatbots() {
657 $chatbots = get_option( $this->chatbots_option_name, [] );
658 $hasChanges = false;
659 if ( empty( $chatbots ) ) {
660 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
661 }
662 foreach ( $chatbots as &$chatbot ) {
663 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
664 // Use default value if not set.
665 if ( !isset( $chatbot[$key] ) ) {
666 $chatbot[$key] = $value;
667 }
668 }
669 // After September 2023, let's remove this if statement.
670 // if ( isset( $chatbot['chatId'] ) ) {
671 // $chatbot['botId'] = $chatbot['chatId'];
672 // unset( $chatbot['chatId'] );
673 // $hasChanges = true;
674 // }
675 // After September 2023, let's remove this if statement.
676 // if ( empty( $chatbot['botId'] ) && $chatbot['name'] === 'default' ) {
677 // $chatbot['botId'] = sanitize_title( $chatbot['name'] );
678 // $hasChanges = true;
679 // }
680 }
681 if ( $hasChanges ) {
682 update_option( $this->chatbots_option_name, $chatbots );
683 }
684 return $chatbots;
685 }
686
687 function get_chatbot( $botId ) {
688 $chatbots = $this->get_chatbots();
689 foreach ( $chatbots as $chatbot ) {
690 if ( $chatbot['botId'] === (string)$botId ) {
691 return $chatbot;
692 }
693 }
694 return null;
695 }
696
697 function get_embeddings_env( $envId ) {
698 $envs = $this->get_option( 'embeddings_envs' );
699 foreach ( $envs as $env ) {
700 if ( $env['id'] === $envId ) {
701 return $env;
702 }
703 }
704 return null;
705 }
706
707 function get_ai_env( $envId ) {
708 $envs = $this->get_option( 'ai_envs' );
709 foreach ( $envs as $env ) {
710 if ( $env['id'] === $envId ) {
711 return $env;
712 }
713 }
714 return null;
715 }
716
717 function get_assistant( $envId, $assistantId ) {
718 $env = $this->get_ai_env( $envId );
719 if ( !$env ) {
720 return null;
721 }
722 $assistants = $env['assistants'];
723 foreach ( $assistants as $assistant ) {
724 if ( $assistant['id'] === $assistantId ) {
725 return $assistant;
726 }
727 }
728 return null;
729 }
730
731 function get_theme( $themeId ) {
732 $themes = $this->get_themes();
733 foreach ( $themes as $theme ) {
734 if ( $theme['themeId'] === $themeId ) {
735 return $theme;
736 }
737 }
738 return null;
739 }
740
741 function update_chatbots( $chatbots ) {
742 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
743 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
744 $whiteSpacedFields = [ 'context' ];
745 foreach ( $chatbots as &$chatbot ) {
746 foreach ( $chatbot as $key => &$value ) {
747 if ( in_array( $key, $deprecatedFields ) ) {
748 unset( $chatbot[$key] );
749 continue;
750 }
751 if ( in_array( $key, $htmlFields ) ) {
752 $value = wp_kses_post( $value );
753 }
754 else if ( in_array( $key, $whiteSpacedFields ) ) {
755 $value = sanitize_textarea_field( $value );
756 }
757 else {
758 $value = sanitize_text_field( $value );
759 }
760 }
761 }
762 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
763 error_log( 'AI Engine: Could not update chatbots.' );
764 $chatbots = get_option( $this->chatbots_option_name, [] );
765 return $chatbots;
766 }
767 return $chatbots;
768 }
769
770 function get_all_options( $force = false ) {
771 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
772 // That causes issues with the mwai_languages filter.
773 // if ( !$force && !is_null( $this->options ) ) {
774 // return $this->options;
775 // }
776 $options = get_option( $this->option_name, [] );
777 $options = $this->sanitize_options( $options );
778 foreach ( MWAI_OPTIONS as $key => $value ) {
779 if ( !isset( $options[$key] ) ) {
780 $options[$key] = $value;
781 }
782 if ( $key === 'languages' ) {
783 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
784 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
785 }
786 }
787 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
788 $options['default_limits'] = MWAI_LIMITS;
789 $options['openai_models'] = apply_filters(
790 'mwai_openai_models',
791 Meow_MWAI_Engines_OpenAI::get_models_static()
792 );
793 $options['anthropic_models'] = apply_filters(
794 'mwai_anthropic_models',
795 Meow_MWAI_Engines_Anthropic::get_models_static()
796 );
797 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
798
799 //$this->options = $options;
800 return $options;
801 }
802
803 // Sanitize options when we update the plugi or perform some updates
804 // if we change the structure of the options.
805 function sanitize_options( $options ) {
806 $needs_update = false;
807
808 // This list was updated on December 11, 2023. After May 2024, let's remove this.
809 $old_options = [
810 'shortcode_chat_default_params',
811 'shortcode_chat_params_override',
812 'module_legacy_finetunes',
813 'shortcode_chat_legacy',
814 'shortcode_chat_inject',
815 'shortcode_chat_styles',
816 'dynamic_max_tokens',
817 'shortcode_chat_formatting',
818 'shortcode_forms_legacy',
819 ];
820 foreach ( $old_options as $old_option ) {
821 if ( isset( $options[$old_option] ) ) {
822 unset( $options[$old_option] );
823 $needs_update = true;
824 }
825 }
826
827 // This upgrades namespace to multi-namespaces (June 2023)
828 // After January 2024, let's remove this.
829 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
830 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
831 unset( $options['pinecone']['namespace'] );
832 $needs_update = true;
833 }
834 // Support for Multi Vector DB Environments
835 // After June 2024, let's remove this.
836 if ( !isset( $options['embeddings_envs'] ) ) {
837 $options['embeddings_envs'] = [];
838 $default_id = $this->get_random_id();
839 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
840 $options['embeddings_envs'][] = [
841 'id' => $default_id,
842 'name' => 'Pinecone',
843 'type' => 'pinecone',
844 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
845 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
846 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
847 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
848 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
849 ];
850 $options['embeddings_default_env'] = $default_id;
851 $needs_update = true;
852 }
853 if ( isset( $options['pinecone'] ) ) {
854 unset( $options['pinecone'] );
855 $needs_update = true;
856 }
857 // Support for Multi AI Environments
858 // After June 2024, let's remove this.
859 if ( !isset( $options['ai_envs'] ) ) {
860 $options['ai_envs'] = [];
861 $default_openai_id = $this->get_random_id();
862 $default_azure_id = $this->get_random_id();
863 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
864 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
865 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
866
867 // OpenAI
868 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
869 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
870 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
871 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
872 $options['openai_finetunes_deleted'] : [];
873 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
874 $options['openai_legacy_finetunes'] : [];
875 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
876 $options['openai_legacy_finetunes_deleted'] : [];
877 $options['ai_envs'][] = [
878 'id' => $default_openai_id,
879 'name' => 'OpenAI',
880 'type' => 'openai',
881 'apikey' => $openai_apikey,
882 'finetunes' => $openai_finetunes,
883 'finetunes_deleted' => $openai_finetunes_deleted,
884 'legacy_finetunes' => $openai_legacy_finetunes,
885 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
886 ];
887 }
888
889 // Azure
890 if ( !empty( $azure_endpoint ) ) {
891 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
892 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
893 $options['ai_envs'][] = [
894 'id' => $default_azure_id,
895 'name' => 'Azure',
896 'type' => 'azure',
897 'apikey' => $azure_apikey,
898 'endpoint' => $azure_endpoint,
899 'deployments' => $azure_deployments,
900 ];
901 }
902
903 $options['ai_default_env'] = $default_openai_id;
904 if ( $openai_service === 'azure' ) {
905 $options['ai_default_env'] = $default_azure_id;
906 }
907 $needs_update = true;
908 }
909 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
910 unset( $options['openai_apikey'] );
911 unset( $options['openai_finetunes'] );
912 unset( $options['openai_finetunes_deleted'] );
913 unset( $options['openai_legacy_finetunes'] );
914 unset( $options['openai_legacy_finetunes_deleted'] );
915 unset( $options['openai_azure_apikey'] );
916 unset( $options['openai_azure_endpoint'] );
917 unset( $options['openai_azure_deployments'] );
918 unset( $options['openai_service'] );
919 $needs_update = true;
920 }
921
922 // The IDs for the embeddings environments are generated here.
923 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
924 $embeddings_default_exists = false;
925 if ( isset( $options['embeddings_envs'] ) ) {
926 foreach ( $options['embeddings_envs'] as &$env ) {
927 if ( !isset( $env['id'] ) ) {
928 $env['id'] = $this->get_random_id();
929 $needs_update = true;
930 }
931 if ( $env['id'] === $options['embeddings_default_env'] ) {
932 $embeddings_default_exists = true;
933 }
934 }
935 }
936 if ( !$embeddings_default_exists ) {
937 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
938 $needs_update = true;
939 }
940
941 // The IDs for the AI environments are generated here.
942 $ai_default_exists = false;
943 if ( isset( $options['ai_envs'] ) ) {
944 foreach ( $options['ai_envs'] as &$env ) {
945 if ( !isset( $env['id'] ) ) {
946 $env['id'] = $this->get_random_id();
947 $needs_update = true;
948 }
949 if ( $env['id'] === $options['ai_default_env'] ) {
950 $ai_default_exists = true;
951 }
952 }
953 }
954 if ( !$ai_default_exists ) {
955 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
956 $needs_update = true;
957 }
958
959 if ( $needs_update ) {
960 update_option( $this->option_name, $options, false );
961 }
962
963 return $options;
964 }
965
966 function update_options( $options ) {
967 if ( !update_option( $this->option_name, $options, false ) ) {
968 return false;
969 }
970 $options = $this->get_all_options( true );
971 return $options;
972 }
973
974 function update_option( $option, $value ) {
975 $options = $this->get_all_options( true );
976 $options[$option] = $value;
977 return $this->update_options( $options );
978 }
979
980 function get_option( $option, $default = null ) {
981 $options = $this->get_all_options();
982 return $options[$option] ?? $default;
983 }
984
985 function update_ai_env( $env_id, $option, $value ) {
986 $options = $this->get_all_options( true );
987 foreach ( $options['ai_envs'] as &$env ) {
988 if ( $env['id'] === $env_id ) {
989 $env[$option] = $value;
990 break;
991 }
992 }
993 return $this->update_options( $options );
994 }
995
996 function reset_options() {
997 delete_option( $this->themes_option_name );
998 delete_option( $this->chatbots_option_name );
999 delete_option( $this->option_name );
1000 return $this->get_all_options( true );
1001 }
1002 #endregion
1003 }
1004
1005 ?>