PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.2.63
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.2.63
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
1040 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 get_mime_type( $file ) {
226 $mimeType = null;
227
228 // Let's try to use mime_content_type if the function exists
229 if ( function_exists( 'mime_content_type' ) ) {
230 $mimeType = mime_content_type( $file );
231 }
232
233 // Otherwise, let's check the file extension (which can actually also be an URL)
234 if ( !$mimeType ) {
235 $extension = pathinfo( $file, PATHINFO_EXTENSION );
236 $extension = strtolower( $extension );
237 $mimeTypes = [
238 'jpg' => 'image/jpeg',
239 'jpeg' => 'image/jpeg',
240 'png' => 'image/png',
241 'gif' => 'image/gif',
242 'webp' => 'image/webp',
243 'bmp' => 'image/bmp',
244 'tiff' => 'image/tiff',
245 'tif' => 'image/tiff',
246 'svg' => 'image/svg+xml',
247 'ico' => 'image/x-icon',
248 'pdf' => 'application/pdf',
249 ];
250 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
251 }
252
253 return $mimeType;
254 }
255
256 function download_image( $url ) {
257 $args = array( 'timeout' => 60, );
258 $response = wp_safe_remote_get( $url, $args );
259 if ( is_wp_error( $response ) ) {
260 throw new Exception( $response->get_error_message() );
261 }
262 $output = wp_remote_retrieve_body( $response );
263 if ( is_wp_error( $output ) ) {
264 throw new Exception( $output->get_error_message() );
265 }
266 return $output;
267 }
268
269 /**
270 * Add an image from a URL to the Media Library.
271 * @param string $url The URL of the image to be downloaded.
272 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
273 * @param string $title The title of the image.
274 * @param string $description The description of the image.
275 * @param string $caption The caption of the image.
276 * @param string $alt The alt text of the image.
277 * @return int The attachment ID of the image.
278 */
279 public function add_image_from_url( $url, $filename = null, $title = null,
280 $description = null, $caption = null, $alt = null ) {
281 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
282 $url_filename = $path_parts['basename'];
283 $file_type = wp_check_filetype( $url_filename, null );
284 $allowed_types = get_allowed_mime_types();
285 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
286 throw new Exception( 'Invalid file type.' );
287 }
288 $extension = $file_type['ext'];
289 $image_data = $this->download_image( $url );
290 if ( !$image_data ) {
291 throw new Exception( 'Could not download the image.' );
292 }
293 $upload_dir = wp_upload_dir();
294
295 // If filename is not set or starts with 'generated_', we will generate a new filename.
296 if ( empty( $filename ) ) {
297 $filename = sanitize_file_name( $url_filename );
298 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
299 if ( empty( $extension ) ) {
300 $extension = $file_type['ext'];
301 }
302 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
303 $filename = $this->get_random_id( 16 ) . '.' . $extension;
304 }
305 if ( strpos( $filename, '.' ) === false ) {
306 $filename .= '.' . $extension;
307 }
308 }
309 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
310 $file = $upload_dir['path'] . '/' . $filename;
311 }
312 else {
313 $file = $upload_dir['basedir'] . '/' . $filename;
314 }
315
316 // Make sure the file is unique, if not, add a number to the end of the file before the extension
317 $i = 1;
318 $parts = pathinfo( $file );
319 while ( file_exists( $file ) ) {
320 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
321 $i++;
322 }
323
324 // Write the file
325 file_put_contents( $file, $image_data );
326 $attachment = [
327 'post_mime_type' => $file_type['type'],
328 'post_title' => $title ?? '',
329 'post_content' => $description ?? '',
330 'post_excerpt' => $caption ?? '',
331 'post_status' => 'inherit'
332 ];
333 // Register the file as a Media Library attachment
334 $attachmentId = wp_insert_attachment( $attachment, $file );
335 require_once( ABSPATH . 'wp-admin/includes/image.php' );
336 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
337 wp_update_attachment_metadata( $attachmentId, $attachment_data );
338 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
339 return $attachmentId;
340 }
341 #endregion
342
343 #region Context-Related Helpers
344 function retrieve_context( $params, $query ) {
345 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
346 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
347 $context = apply_filters( 'mwai_context_search', [], $query, [
348 'embeddingsEnvId' => $embeddingsEnvId
349 ]);
350 if ( empty( $context ) ) {
351 return null;
352 }
353 else if ( !isset( $context['content'] ) ) {
354 error_log( "AI Engine: A context without content was returned." );
355 return null;
356 }
357 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
358 $context['length'] = strlen( $context['content'] );
359 return $context;
360 }
361 #endregion
362
363 #region Users/Sessions Helpers
364
365 function get_nonce() {
366 // if ( !is_user_logged_in() ) {
367 // return null;
368 // }
369 if ( isset( $this->nonce ) ) {
370 return $this->nonce;
371 }
372 $this->nonce = wp_create_nonce( 'wp_rest' );
373 return $this->nonce;
374 }
375
376 function get_session_id() {
377 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
378 return $_COOKIE['mwai_session_id'];
379 }
380 return "N/A";
381 }
382
383 // Get the UserID from the data, or from the current user
384 function get_user_id( $data = null ) {
385 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
386 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
387 $admin = get_users( [ 'role' => 'administrator' ] );
388 if ( !empty( $admin ) ) {
389 return $admin[0]->ID;
390 }
391 }
392 if ( isset( $data ) && isset( $data['userId'] ) ) {
393 return (int)$data['userId'];
394 }
395 if ( is_user_logged_in() ) {
396 $current_user = wp_get_current_user();
397 if ( $current_user->ID > 0 ) {
398 return $current_user->ID;
399 }
400 }
401 return null;
402 }
403
404 function get_admin_user() {
405 $admin = get_users( [ 'role' => 'administrator' ] );
406 if ( !empty( $admin ) ) {
407 return $admin[0];
408 }
409 return null;
410 }
411
412 function get_user_data() {
413 $user = wp_get_current_user();
414 if ( empty( $user ) || empty( $user->ID ) ) {
415 return null;
416 }
417 $placeholders = array(
418 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
419 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
420 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
421 $user->data->user_login : null,
422 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
423 $user->data->display_name : null,
424 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
425 );
426 return $placeholders;
427 }
428
429 function get_ip_address( $params = null ) {
430 $ip = '127.0.0.1';
431 $headers = [
432 'HTTP_TRUE_CLIENT_IP',
433 'HTTP_CF_CONNECTING_IP',
434 'HTTP_X_REAL_IP',
435 'HTTP_CLIENT_IP',
436 'HTTP_X_FORWARDED_FOR',
437 'HTTP_X_FORWARDED',
438 'HTTP_X_CLUSTER_CLIENT_IP',
439 'HTTP_FORWARDED_FOR',
440 'HTTP_FORWARDED',
441 'REMOTE_ADDR',
442 ];
443
444 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
445 $ip = ( string )$params[ 'ip' ];
446 } else {
447 foreach ( $headers as $header ) {
448 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
449 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
450 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
451 break;
452 }
453 }
454 }
455
456 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
457 }
458
459 #endregion
460
461 #region Other Helpers
462
463 public function check_rest_nonce( $request ) {
464 $nonce = $request->get_header( 'X-WP-Nonce' );
465 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
466 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
467 }
468
469 function get_random_id( $length = 8, $excludeIds = [] ) {
470 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
471 $charactersLength = strlen( $characters );
472 $randomId = '';
473 for ( $i = 0; $i < $length; $i++ ) {
474 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
475 }
476 if ( in_array( $randomId, $excludeIds ) ) {
477 return $this->get_random_id( $length, $excludeIds );
478 }
479 return $randomId;
480 }
481
482 function is_url( $url ) {
483 return strpos( $url, 'http' ) === 0 ? true : false;
484 }
485
486 function get_post_types() {
487 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
488 $post_types = array();
489 $types = get_post_types( [], 'objects' );
490
491 // Let's get the Post Types that are enabled for Embeddings Sync
492 $embeddingsSettings = $this->get_option( 'embeddings' );
493 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
494
495 foreach ( $types as $type ) {
496 $forced = in_array( $type->name, $syncPostTypes );
497 // Should not be excluded.
498 if ( !$forced && in_array( $type->name, $excluded ) ) {
499 continue;
500 }
501 // Should be public.
502 if ( !$forced && !$type->public ) {
503 continue;
504 }
505 $post_types[] = array(
506 'name' => $type->labels->name,
507 'type' => $type->name,
508 );
509 }
510
511 // Let's get the Post Types that are enabled for Embeddings Sync
512 $embeddingsSettings = $this->get_option( 'embeddings' );
513 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
514
515 return $post_types;
516 }
517
518 function get_post( $post ) {
519 if ( is_numeric( $post ) ) {
520 $post = get_post( $post );
521 }
522 if ( is_object( $post ) ) {
523 $post = (array)$post;
524 }
525 if ( !is_array( $post ) ) {
526 return null;
527 }
528 $language = $this->get_post_language( $post['ID'] );
529 $content = $this->get_post_content( $post['ID'] );
530 $title = $post['post_title'];
531 $excerpt = $post['post_excerpt'];
532 $url = get_permalink( $post['ID'] );
533 $checksum = wp_hash( $content . $title . $url );
534 return [
535 'postId' => (int)$post['ID'],
536 'title' => $title,
537 'content' => $content,
538 'excerpt' => $excerpt,
539 'url' => $url,
540 'language' => $language ?? 'english',
541 'checksum' => $checksum,
542 ];
543 }
544 #endregion
545
546 #region Usage & Costs
547
548 // Quick and dirty token estimation
549 // Let's keep this synchronized with Helpers in JS
550 static function estimate_tokens( ...$args ): int {
551 $text = "";
552 foreach ( $args as $arg ) {
553 if ( is_array( $arg ) ) {
554 foreach ( $arg as $message ) {
555 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
556 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
557 }
558 }
559 else if ( is_string( $arg ) ) {
560 $text .= $arg;
561 }
562 }
563 $averageTokenLength = 4;
564 $words = preg_split( '/\s+/', trim( $text ) );
565 $tokenCount = 0;
566 foreach ( $words as $word ) {
567 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
568 }
569 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
570 }
571
572 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
573 if ( !is_numeric( $in_tokens ) ) {
574 throw new Exception( 'AI Engine: in_tokens must be a number.' );
575 }
576 if ( !is_numeric( $out_tokens ) ) {
577 $out_tokens = 0;
578 }
579 if ( !$model ) {
580 throw new Exception( 'AI Engine: model is required.' );
581 }
582 $usage = $this->get_option( 'openai_usage' );
583 $month = date( 'Y-m' );
584 if ( !isset( $usage[$month] ) ) {
585 $usage[$month] = array();
586 }
587 if ( !isset( $usage[$month][$model] ) ) {
588 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
589 }
590 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
591 $usage[$month][$model]['completion_tokens'] += $out_tokens;
592 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
593 $this->update_option( 'openai_usage', $usage );
594 $usageInfo = [
595 'prompt_tokens' => $in_tokens,
596 'completion_tokens' => $out_tokens,
597 'total_tokens' => $in_tokens + $out_tokens,
598 ];
599 if ( $returned_price !== null ) {
600 $usageInfo['price'] = $returned_price;
601 }
602 return $usageInfo;
603 }
604
605 public function record_audio_usage( $model, $seconds ) {
606 if ( !is_numeric( $seconds ) ) {
607 throw new Exception( 'AI Engine: seconds must be a number.' );
608 }
609 if ( !$model ) {
610 throw new Exception( 'AI Engine: model is required.' );
611 }
612 $usage = $this->get_option( 'openai_usage' );
613 $month = date( 'Y-m' );
614 if ( !isset( $usage[$month] ) ) {
615 $usage[$month] = array();
616 }
617 if ( !isset( $usage[$month][$model] ) ) {
618 $usage[$month][$model] = array( 'seconds' => 0 );
619 }
620 $usage[$month][$model]['seconds'] += $seconds;
621 $this->update_option( 'openai_usage', $usage );
622 return [ 'seconds' => $seconds ];
623 }
624
625 public function record_images_usage( $model, $resolution, $images ) {
626 if ( !$model || !$resolution || !$images ) {
627 throw new Exception( 'Missing parameters for record_image_usage.' );
628 }
629 $usage = $this->get_option( 'openai_usage' );
630 $month = date( 'Y-m' );
631 if ( !isset( $usage[$month] ) ) {
632 $usage[$month] = array();
633 }
634 if ( !isset( $usage[$month][$model] ) ) {
635 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
636 }
637 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
638 $usage[$month][$model]['resolution'][$resolution] = 0;
639 }
640 $usage[$month][$model]['resolution'][$resolution] += $images;
641 $usage[$month][$model]['images'] += $images;
642 $this->update_option( 'openai_usage', $usage );
643 return [ 'resolution' => $resolution, 'images' => $images ];
644 }
645
646 #endregion
647
648 #region Streaming
649 public function stream_push( $data ) {
650 $out = "data: " . json_encode( $data );
651 echo $out;
652 echo "\n\n";
653 if ( ob_get_level() > 0 ) {
654 ob_end_flush();
655 }
656 flush();
657 }
658 #endregion
659
660 #region Options
661 function get_themes() {
662 $themes = get_option( $this->themes_option_name, [] );
663 $themes = empty( $themes ) ? [] : $themes;
664
665 $internalThemes = [
666 'chatgpt' => [
667 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
668 'settings' => [], 'style' => ""
669 ],
670 'messages' => [
671 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
672 'settings' => [], 'style' => ""
673 ],
674 ];
675 $customThemes = [];
676 foreach ( $themes as $theme ) {
677 if ( isset( $internalThemes[$theme['themeId']] ) ) {
678 $internalThemes[$theme['themeId']] = $theme;
679 continue;
680 }
681 $customThemes[] = $theme;
682 }
683 return array_merge(array_values($internalThemes), $customThemes);
684 }
685
686 function update_themes( $themes ) {
687 update_option( $this->themes_option_name, $themes );
688 return $themes;
689 }
690
691 function get_chatbots() {
692 $chatbots = get_option( $this->chatbots_option_name, [] );
693 $hasChanges = false;
694 if ( empty( $chatbots ) ) {
695 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
696 }
697 foreach ( $chatbots as &$chatbot ) {
698 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
699 // Use default value if not set.
700 if ( !isset( $chatbot[$key] ) ) {
701 $chatbot[$key] = $value;
702 }
703 }
704 // After September 2023, let's remove this if statement.
705 // if ( isset( $chatbot['chatId'] ) ) {
706 // $chatbot['botId'] = $chatbot['chatId'];
707 // unset( $chatbot['chatId'] );
708 // $hasChanges = true;
709 // }
710 // After September 2023, let's remove this if statement.
711 // if ( empty( $chatbot['botId'] ) && $chatbot['name'] === 'default' ) {
712 // $chatbot['botId'] = sanitize_title( $chatbot['name'] );
713 // $hasChanges = true;
714 // }
715 }
716 if ( $hasChanges ) {
717 update_option( $this->chatbots_option_name, $chatbots );
718 }
719 return $chatbots;
720 }
721
722 function get_chatbot( $botId ) {
723 $chatbots = $this->get_chatbots();
724 foreach ( $chatbots as $chatbot ) {
725 if ( $chatbot['botId'] === (string)$botId ) {
726 return $chatbot;
727 }
728 }
729 return null;
730 }
731
732 function get_embeddings_env( $envId ) {
733 $envs = $this->get_option( 'embeddings_envs' );
734 foreach ( $envs as $env ) {
735 if ( $env['id'] === $envId ) {
736 return $env;
737 }
738 }
739 return null;
740 }
741
742 function get_ai_env( $envId ) {
743 $envs = $this->get_option( 'ai_envs' );
744 foreach ( $envs as $env ) {
745 if ( $env['id'] === $envId ) {
746 return $env;
747 }
748 }
749 return null;
750 }
751
752 function get_assistant( $envId, $assistantId ) {
753 $env = $this->get_ai_env( $envId );
754 if ( !$env ) {
755 return null;
756 }
757 $assistants = $env['assistants'];
758 foreach ( $assistants as $assistant ) {
759 if ( $assistant['id'] === $assistantId ) {
760 return $assistant;
761 }
762 }
763 return null;
764 }
765
766 function get_theme( $themeId ) {
767 $themes = $this->get_themes();
768 foreach ( $themes as $theme ) {
769 if ( $theme['themeId'] === $themeId ) {
770 return $theme;
771 }
772 }
773 return null;
774 }
775
776 function update_chatbots( $chatbots ) {
777 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
778 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
779 $whiteSpacedFields = [ 'context' ];
780 foreach ( $chatbots as &$chatbot ) {
781 foreach ( $chatbot as $key => &$value ) {
782 if ( in_array( $key, $deprecatedFields ) ) {
783 unset( $chatbot[$key] );
784 continue;
785 }
786 if ( in_array( $key, $htmlFields ) ) {
787 $value = wp_kses_post( $value );
788 }
789 else if ( in_array( $key, $whiteSpacedFields ) ) {
790 $value = sanitize_textarea_field( $value );
791 }
792 else {
793 $value = sanitize_text_field( $value );
794 }
795 }
796 }
797 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
798 error_log( 'AI Engine: Could not update chatbots.' );
799 $chatbots = get_option( $this->chatbots_option_name, [] );
800 return $chatbots;
801 }
802 return $chatbots;
803 }
804
805 function get_all_options( $force = false ) {
806 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
807 // That causes issues with the mwai_languages filter.
808 // if ( !$force && !is_null( $this->options ) ) {
809 // return $this->options;
810 // }
811 $options = get_option( $this->option_name, [] );
812 $options = $this->sanitize_options( $options );
813 foreach ( MWAI_OPTIONS as $key => $value ) {
814 if ( !isset( $options[$key] ) ) {
815 $options[$key] = $value;
816 }
817 if ( $key === 'languages' ) {
818 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
819 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
820 }
821 }
822 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
823 $options['default_limits'] = MWAI_LIMITS;
824 $options['openai_models'] = apply_filters(
825 'mwai_openai_models',
826 Meow_MWAI_Engines_OpenAI::get_models_static()
827 );
828 $options['anthropic_models'] = apply_filters(
829 'mwai_anthropic_models',
830 Meow_MWAI_Engines_Anthropic::get_models_static()
831 );
832 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
833
834 //$this->options = $options;
835 return $options;
836 }
837
838 // Sanitize options when we update the plugi or perform some updates
839 // if we change the structure of the options.
840 function sanitize_options( $options ) {
841 $needs_update = false;
842
843 // This list was updated on December 11, 2023. After May 2024, let's remove this.
844 $old_options = [
845 'shortcode_chat_default_params',
846 'shortcode_chat_params_override',
847 'module_legacy_finetunes',
848 'shortcode_chat_legacy',
849 'shortcode_chat_inject',
850 'shortcode_chat_styles',
851 'dynamic_max_tokens',
852 'shortcode_chat_formatting',
853 'shortcode_forms_legacy',
854 ];
855 foreach ( $old_options as $old_option ) {
856 if ( isset( $options[$old_option] ) ) {
857 unset( $options[$old_option] );
858 $needs_update = true;
859 }
860 }
861
862 // This upgrades namespace to multi-namespaces (June 2023)
863 // After January 2024, let's remove this.
864 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
865 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
866 unset( $options['pinecone']['namespace'] );
867 $needs_update = true;
868 }
869 // Support for Multi Vector DB Environments
870 // After June 2024, let's remove this.
871 if ( !isset( $options['embeddings_envs'] ) ) {
872 $options['embeddings_envs'] = [];
873 $default_id = $this->get_random_id();
874 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
875 $options['embeddings_envs'][] = [
876 'id' => $default_id,
877 'name' => 'Pinecone',
878 'type' => 'pinecone',
879 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
880 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
881 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
882 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
883 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
884 ];
885 $options['embeddings_default_env'] = $default_id;
886 $needs_update = true;
887 }
888 if ( isset( $options['pinecone'] ) ) {
889 unset( $options['pinecone'] );
890 $needs_update = true;
891 }
892 // Support for Multi AI Environments
893 // After June 2024, let's remove this.
894 if ( !isset( $options['ai_envs'] ) ) {
895 $options['ai_envs'] = [];
896 $default_openai_id = $this->get_random_id();
897 $default_azure_id = $this->get_random_id();
898 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
899 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
900 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
901
902 // OpenAI
903 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
904 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
905 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
906 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
907 $options['openai_finetunes_deleted'] : [];
908 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
909 $options['openai_legacy_finetunes'] : [];
910 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
911 $options['openai_legacy_finetunes_deleted'] : [];
912 $options['ai_envs'][] = [
913 'id' => $default_openai_id,
914 'name' => 'OpenAI',
915 'type' => 'openai',
916 'apikey' => $openai_apikey,
917 'finetunes' => $openai_finetunes,
918 'finetunes_deleted' => $openai_finetunes_deleted,
919 'legacy_finetunes' => $openai_legacy_finetunes,
920 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
921 ];
922 }
923
924 // Azure
925 if ( !empty( $azure_endpoint ) ) {
926 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
927 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
928 $options['ai_envs'][] = [
929 'id' => $default_azure_id,
930 'name' => 'Azure',
931 'type' => 'azure',
932 'apikey' => $azure_apikey,
933 'endpoint' => $azure_endpoint,
934 'deployments' => $azure_deployments,
935 ];
936 }
937
938 $options['ai_default_env'] = $default_openai_id;
939 if ( $openai_service === 'azure' ) {
940 $options['ai_default_env'] = $default_azure_id;
941 }
942 $needs_update = true;
943 }
944 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
945 unset( $options['openai_apikey'] );
946 unset( $options['openai_finetunes'] );
947 unset( $options['openai_finetunes_deleted'] );
948 unset( $options['openai_legacy_finetunes'] );
949 unset( $options['openai_legacy_finetunes_deleted'] );
950 unset( $options['openai_azure_apikey'] );
951 unset( $options['openai_azure_endpoint'] );
952 unset( $options['openai_azure_deployments'] );
953 unset( $options['openai_service'] );
954 $needs_update = true;
955 }
956
957 // The IDs for the embeddings environments are generated here.
958 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
959 $embeddings_default_exists = false;
960 if ( isset( $options['embeddings_envs'] ) ) {
961 foreach ( $options['embeddings_envs'] as &$env ) {
962 if ( !isset( $env['id'] ) ) {
963 $env['id'] = $this->get_random_id();
964 $needs_update = true;
965 }
966 if ( $env['id'] === $options['embeddings_default_env'] ) {
967 $embeddings_default_exists = true;
968 }
969 }
970 }
971 if ( !$embeddings_default_exists ) {
972 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
973 $needs_update = true;
974 }
975
976 // The IDs for the AI environments are generated here.
977 $ai_default_exists = false;
978 if ( isset( $options['ai_envs'] ) ) {
979 foreach ( $options['ai_envs'] as &$env ) {
980 if ( !isset( $env['id'] ) ) {
981 $env['id'] = $this->get_random_id();
982 $needs_update = true;
983 }
984 if ( $env['id'] === $options['ai_default_env'] ) {
985 $ai_default_exists = true;
986 }
987 }
988 }
989 if ( !$ai_default_exists ) {
990 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
991 $needs_update = true;
992 }
993
994 if ( $needs_update ) {
995 update_option( $this->option_name, $options, false );
996 }
997
998 return $options;
999 }
1000
1001 function update_options( $options ) {
1002 if ( !update_option( $this->option_name, $options, false ) ) {
1003 return false;
1004 }
1005 $options = $this->get_all_options( true );
1006 return $options;
1007 }
1008
1009 function update_option( $option, $value ) {
1010 $options = $this->get_all_options( true );
1011 $options[$option] = $value;
1012 return $this->update_options( $options );
1013 }
1014
1015 function get_option( $option, $default = null ) {
1016 $options = $this->get_all_options();
1017 return $options[$option] ?? $default;
1018 }
1019
1020 function update_ai_env( $env_id, $option, $value ) {
1021 $options = $this->get_all_options( true );
1022 foreach ( $options['ai_envs'] as &$env ) {
1023 if ( $env['id'] === $env_id ) {
1024 $env[$option] = $value;
1025 break;
1026 }
1027 }
1028 return $this->update_options( $options );
1029 }
1030
1031 function reset_options() {
1032 delete_option( $this->themes_option_name );
1033 delete_option( $this->chatbots_option_name );
1034 delete_option( $this->option_name );
1035 return $this->get_all_options( true );
1036 }
1037 #endregion
1038 }
1039
1040 ?>