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