PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.2.51
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.2.51
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
997 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 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
743 $whiteSpacedFields = [ 'context' ];
744 foreach ( $chatbots as &$chatbot ) {
745 foreach ( $chatbot as $key => &$value ) {
746 if ( in_array( $key, $htmlFields ) ) {
747 $value = wp_kses_post( $value );
748 }
749 else if ( in_array( $key, $whiteSpacedFields ) ) {
750 $value = sanitize_textarea_field( $value );
751 }
752 else {
753 $value = sanitize_text_field( $value );
754 }
755 }
756 }
757
758 update_option( $this->chatbots_option_name, $chatbots );
759 return $chatbots;
760 }
761
762 function get_all_options( $force = false ) {
763 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
764 // That causes issues with the mwai_languages filter.
765 // if ( !$force && !is_null( $this->options ) ) {
766 // return $this->options;
767 // }
768 $options = get_option( $this->option_name, [] );
769 $options = $this->sanitize_options( $options );
770 foreach ( MWAI_OPTIONS as $key => $value ) {
771 if ( !isset( $options[$key] ) ) {
772 $options[$key] = $value;
773 }
774 if ( $key === 'languages' ) {
775 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
776 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
777 }
778 }
779 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
780 $options['default_limits'] = MWAI_LIMITS;
781 $options['openai_models'] = apply_filters(
782 'mwai_openai_models',
783 Meow_MWAI_Engines_OpenAI::get_models_static()
784 );
785 $options['anthropic_models'] = apply_filters(
786 'mwai_anthropic_models',
787 Meow_MWAI_Engines_Anthropic::get_models_static()
788 );
789 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
790
791 //$this->options = $options;
792 return $options;
793 }
794
795 // Sanitize options when we update the plugi or perform some updates
796 // if we change the structure of the options.
797 function sanitize_options( $options ) {
798 $needs_update = false;
799
800 // This list was updated on December 11, 2023. After May 2024, let's remove this.
801 $old_options = [
802 'shortcode_chat_default_params',
803 'shortcode_chat_params_override',
804 'module_legacy_finetunes',
805 'shortcode_chat_legacy',
806 'shortcode_chat_inject',
807 'shortcode_chat_styles',
808 'dynamic_max_tokens',
809 'shortcode_chat_formatting',
810 'shortcode_forms_legacy',
811 ];
812 foreach ( $old_options as $old_option ) {
813 if ( isset( $options[$old_option] ) ) {
814 unset( $options[$old_option] );
815 $needs_update = true;
816 }
817 }
818
819 // This upgrades namespace to multi-namespaces (June 2023)
820 // After January 2024, let's remove this.
821 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
822 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
823 unset( $options['pinecone']['namespace'] );
824 $needs_update = true;
825 }
826 // Support for Multi Vector DB Environments
827 // After June 2024, let's remove this.
828 if ( !isset( $options['embeddings_envs'] ) ) {
829 $options['embeddings_envs'] = [];
830 $default_id = $this->get_random_id();
831 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
832 $options['embeddings_envs'][] = [
833 'id' => $default_id,
834 'name' => 'Pinecone',
835 'type' => 'pinecone',
836 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
837 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
838 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
839 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
840 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
841 ];
842 $options['embeddings_default_env'] = $default_id;
843 $needs_update = true;
844 }
845 if ( isset( $options['pinecone'] ) ) {
846 unset( $options['pinecone'] );
847 $needs_update = true;
848 }
849 // Support for Multi AI Environments
850 // After June 2024, let's remove this.
851 if ( !isset( $options['ai_envs'] ) ) {
852 $options['ai_envs'] = [];
853 $default_openai_id = $this->get_random_id();
854 $default_azure_id = $this->get_random_id();
855 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
856 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
857 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
858
859 // OpenAI
860 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
861 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
862 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
863 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
864 $options['openai_finetunes_deleted'] : [];
865 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
866 $options['openai_legacy_finetunes'] : [];
867 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
868 $options['openai_legacy_finetunes_deleted'] : [];
869 $options['ai_envs'][] = [
870 'id' => $default_openai_id,
871 'name' => 'OpenAI',
872 'type' => 'openai',
873 'apikey' => $openai_apikey,
874 'finetunes' => $openai_finetunes,
875 'finetunes_deleted' => $openai_finetunes_deleted,
876 'legacy_finetunes' => $openai_legacy_finetunes,
877 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
878 ];
879 }
880
881 // Azure
882 if ( !empty( $azure_endpoint ) ) {
883 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
884 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
885 $options['ai_envs'][] = [
886 'id' => $default_azure_id,
887 'name' => 'Azure',
888 'type' => 'azure',
889 'apikey' => $azure_apikey,
890 'endpoint' => $azure_endpoint,
891 'deployments' => $azure_deployments,
892 ];
893 }
894
895 $options['ai_default_env'] = $default_openai_id;
896 if ( $openai_service === 'azure' ) {
897 $options['ai_default_env'] = $default_azure_id;
898 }
899 $needs_update = true;
900 }
901 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
902 unset( $options['openai_apikey'] );
903 unset( $options['openai_finetunes'] );
904 unset( $options['openai_finetunes_deleted'] );
905 unset( $options['openai_legacy_finetunes'] );
906 unset( $options['openai_legacy_finetunes_deleted'] );
907 unset( $options['openai_azure_apikey'] );
908 unset( $options['openai_azure_endpoint'] );
909 unset( $options['openai_azure_deployments'] );
910 unset( $options['openai_service'] );
911 $needs_update = true;
912 }
913
914 // The IDs for the embeddings environments are generated here.
915 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
916 $embeddings_default_exists = false;
917 if ( isset( $options['embeddings_envs'] ) ) {
918 foreach ( $options['embeddings_envs'] as &$env ) {
919 if ( !isset( $env['id'] ) ) {
920 $env['id'] = $this->get_random_id();
921 $needs_update = true;
922 }
923 if ( $env['id'] === $options['embeddings_default_env'] ) {
924 $embeddings_default_exists = true;
925 }
926 }
927 }
928 if ( !$embeddings_default_exists ) {
929 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
930 $needs_update = true;
931 }
932
933 // The IDs for the AI environments are generated here.
934 $ai_default_exists = false;
935 if ( isset( $options['ai_envs'] ) ) {
936 foreach ( $options['ai_envs'] as &$env ) {
937 if ( !isset( $env['id'] ) ) {
938 $env['id'] = $this->get_random_id();
939 $needs_update = true;
940 }
941 if ( $env['id'] === $options['ai_default_env'] ) {
942 $ai_default_exists = true;
943 }
944 }
945 }
946 if ( !$ai_default_exists ) {
947 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
948 $needs_update = true;
949 }
950
951 if ( $needs_update ) {
952 update_option( $this->option_name, $options, false );
953 }
954
955 return $options;
956 }
957
958 function update_options( $options ) {
959 if ( !update_option( $this->option_name, $options, false ) ) {
960 return false;
961 }
962 $options = $this->get_all_options( true );
963 return $options;
964 }
965
966 function update_option( $option, $value ) {
967 $options = $this->get_all_options( true );
968 $options[$option] = $value;
969 return $this->update_options( $options );
970 }
971
972 function get_option( $option, $default = null ) {
973 $options = $this->get_all_options();
974 return $options[$option] ?? $default;
975 }
976
977 function update_ai_env( $env_id, $option, $value ) {
978 $options = $this->get_all_options( true );
979 foreach ( $options['ai_envs'] as &$env ) {
980 if ( $env['id'] === $env_id ) {
981 $env[$option] = $value;
982 break;
983 }
984 }
985 return $this->update_options( $options );
986 }
987
988 function reset_options() {
989 delete_option( $this->themes_option_name );
990 delete_option( $this->chatbots_option_name );
991 delete_option( $this->option_name );
992 return $this->get_all_options( true );
993 }
994 #endregion
995 }
996
997 ?>