PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.1.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.1.9
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
970 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 private $option_name = 'mwai_options';
20 private $themes_option_name = 'mwai_themes';
21 private $chatbots_option_name = 'mwai_chatbots';
22 private $nonce = null;
23
24 public $chatbot = null;
25 public $discussions = null;
26
27 public function __construct() {
28 $this->site_url = get_site_url();
29 $this->is_rest = MeowCommon_Helpers::is_rest();
30 $this->is_cli = defined( 'WP_CLI' );
31 $this->files = new Meow_MWAI_Modules_Files( $this );
32
33 add_action( 'plugins_loaded', array( $this, 'init' ) );
34 add_action( 'wp_register_script', array( $this, 'register_scripts' ) );
35 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
36 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
37 }
38
39 #region Init & Scripts
40 function init() {
41 global $mwai;
42 $this->chatbot = null;
43 $this->discussions = null;
44 new Meow_MWAI_Modules_Security( $this );
45 if ( $this->is_rest ) {
46 new Meow_MWAI_Rest( $this );
47 }
48 if ( is_admin() ) {
49 new Meow_MWAI_Admin( $this );
50 new Meow_MWAI_Modules_Utilities( $this );
51 }
52 if ( $this->get_option( 'shortcode_chat' ) ) {
53 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
54 $this->discussions = new Meow_MWAI_Modules_Discussions();
55 }
56
57 // Advanced core
58 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
59 new MeowPro_MWAI_Core( $this );
60 }
61
62 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
63 }
64
65 public function register_scripts() {
66 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
67 }
68
69 public function enqueue_scripts() {
70 $this->register_scripts();
71 wp_enqueue_script( "mwai_highlight" );
72 }
73
74 #endregion
75
76 #region Roles & Capabilities
77
78 function can_access_settings() {
79 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
80 }
81
82 function can_access_features() {
83 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
84 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
85 }
86
87 function can_access_public_api( $feature, $extra ) {
88 $logged_in = is_user_logged_in();
89 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
90 }
91
92 #endregion
93
94 #region AI-Related Helpers
95 function run_query( $query, $streamCallback = null, $markdown = false ) {
96 $envId = !empty( $query->envId ) ? $query->envId : null;
97 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
98
99 // If the engine is not set, we need to set it to the default one.
100 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
101 if ( $query instanceof Meow_MWAI_Query_Text ) {
102 $this->set_if_empty_defaults( $query, 'ai_default_env', 'ai_default_model' );
103 }
104 if ( $query instanceof Meow_MWAI_Query_Embed ) {
105 $this->set_if_empty_defaults( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
106 }
107 else if ( $query instanceof Meow_MWAI_Query_Image ) {
108 $this->set_if_empty_defaults( $query, 'ai_images_default_env', 'ai_images_default_model' );
109 }
110 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
111 $this->set_if_empty_defaults( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
112 }
113 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
114 }
115
116 // Let's run the query.
117 $reply = $engine->run( $query, $streamCallback );
118
119 // Let's allow to modify the reply before it is sent.
120 if ( $markdown ) {
121 if ( $query instanceof Meow_MWAI_Query_Image ) {
122 $reply->result = "";
123 foreach ( $reply->results as $result ) {
124 $reply->result .= "![Image]($result)\n";
125 }
126 }
127 }
128
129 return $reply;
130 }
131
132 private function set_if_empty_defaults( $query, $envOption, $modelOption ) {
133 $defaultEnv = $this->get_option( $envOption );
134 $defaultModel = $this->get_option( $modelOption );
135 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
136 throw new Exception( 'AI Engine: The default environment and model are not set.' );
137 }
138 $query->set_env_id( $defaultEnv );
139 $query->set_model( $defaultModel );
140 }
141 #endregion
142
143 #region Text-Related Helpers
144
145 // Clean the text perfectly, resolve shortcodes, etc, etc.
146 function clean_text( $rawText = "" ) {
147 $text = html_entity_decode( $rawText );
148 $text = wp_strip_all_tags( $text );
149 $text = preg_replace( '/[\r\n]+/', "\n", $text );
150 $text = preg_replace( '/\n+/', "\n", $text );
151 $text = preg_replace( '/\t+/', "\t", $text );
152 return $text . " ";
153 }
154
155 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
156 function clean_sentences( $text, $maxLength = null ) {
157 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
158 $sentences = preg_split('/(?<=[.?!。.!?])+/u', $text);
159 $hashes = array();
160 $uniqueSentences = array();
161 $total = 0;
162 foreach ( $sentences as $sentence ) {
163 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
164 $hash = md5( $sentence );
165 if ( !in_array( $hash, $hashes ) ) {
166 $length = strlen( $sentence );
167 if ( $total + $length > $maxLength ) {
168 continue;
169 }
170 $hashes[] = $hash;
171 $uniqueSentences[] = $sentence;
172 $total += $length;
173 }
174 }
175 $freshText = implode( " ", $uniqueSentences );
176 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
177 return $freshText;
178 }
179
180 function get_post_content( $postId ) {
181 $post = get_post( $postId );
182 if ( !$post ) {
183 return false;
184 }
185 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
186 $pattern = '/\[mwai_.*?\]/';
187 $text = preg_replace( $pattern, '', $text );
188 if ( $this->get_option( 'resolve_shortcodes' ) ) {
189 $text = apply_filters( 'the_content', $text );
190 }
191 else {
192 $pattern = "/\[[^\]]+\]/";
193 $text = preg_replace( $pattern, '', $text );
194 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
195 $text = preg_replace( $pattern, '', $text );
196 }
197 $text = $this->clean_text( $text );
198 $text = $this->clean_sentences( $text );
199 $text = apply_filters( 'mwai_post_content', $text, $postId );
200 return $text;
201 }
202
203 function markdown_to_html( $content ) {
204 $Parsedown = new Parsedown();
205 $content = $Parsedown->text( $content );
206 return $content;
207 }
208
209 function get_post_language( $postId ) {
210 $locale = get_locale();
211 $code = strtolower( substr( $locale, 0, 2 ) );
212 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
213 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
214 if ( !empty( $lang ) ) {
215 $locale = $lang['locale'];
216 $humanLanguage = $lang['display_name'];
217 }
218 return strtolower( "$locale ($humanLanguage)" );
219 }
220 #endregion
221
222 #region Image-Related Helpers
223 function download_image( $url ) {
224 $args = array( 'timeout' => 60, );
225 $response = wp_safe_remote_get( $url, $args );
226 if ( is_wp_error( $response ) ) {
227 throw new Exception( $response->get_error_message() );
228 }
229 $output = wp_remote_retrieve_body( $response );
230 if ( is_wp_error( $output ) ) {
231 throw new Exception( $output->get_error_message() );
232 }
233 return $output;
234 }
235
236 /**
237 * Add an image from a URL to the Media Library.
238 * @param string $url The URL of the image to be downloaded.
239 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
240 * @param string $title The title of the image.
241 * @param string $description The description of the image.
242 * @param string $caption The caption of the image.
243 * @param string $alt The alt text of the image.
244 * @return int The attachment ID of the image.
245 */
246 public function add_image_from_url( $url, $filename = null, $title = null,
247 $description = null, $caption = null, $alt = null ) {
248 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
249 $url_filename = $path_parts['basename'];
250 $file_type = wp_check_filetype( $url_filename, null );
251 $allowed_types = get_allowed_mime_types();
252 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
253 throw new Exception( 'Invalid file type.' );
254 }
255 $extension = $file_type['ext'];
256 $image_data = $this->download_image( $url );
257 if ( !$image_data ) {
258 throw new Exception( 'Could not download the image.' );
259 }
260 $upload_dir = wp_upload_dir();
261
262 // If filename is not set or starts with 'generated_', we will generate a new filename.
263 if ( empty( $filename ) ) {
264 $filename = sanitize_file_name( $url_filename );
265 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
266 if ( empty( $extension ) ) {
267 $extension = $file_type['ext'];
268 }
269 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
270 $filename = $this->get_random_id( 16 ) . '.' . $extension;
271 }
272 if ( strpos( $filename, '.' ) === false ) {
273 $filename .= '.' . $extension;
274 }
275 }
276 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
277 $file = $upload_dir['path'] . '/' . $filename;
278 }
279 else {
280 $file = $upload_dir['basedir'] . '/' . $filename;
281 }
282
283 // Make sure the file is unique, if not, add a number to the end of the file before the extension
284 $i = 1;
285 $parts = pathinfo( $file );
286 while ( file_exists( $file ) ) {
287 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
288 $i++;
289 }
290
291 // Write the file
292 file_put_contents( $file, $image_data );
293 $attachment = [
294 'post_mime_type' => $file_type['type'],
295 'post_title' => $title ?? '',
296 'post_content' => $description ?? '',
297 'post_excerpt' => $caption ?? '',
298 'post_status' => 'inherit'
299 ];
300 // Register the file as a Media Library attachment
301 $attachmentId = wp_insert_attachment( $attachment, $file );
302 require_once( ABSPATH . 'wp-admin/includes/image.php' );
303 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
304 wp_update_attachment_metadata( $attachmentId, $attachment_data );
305 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
306 return $attachmentId;
307 }
308 #endregion
309
310 #region Context-Related Helpers
311 function retrieve_context( $params, $query ) {
312 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
313 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
314 $embeddingsIndex = $params['embeddingsIndex'] ?? null;
315 $embeddingsNamespace = $params['embeddingsNamespace'] ?? null;
316 $context = apply_filters( 'mwai_context_search', [], $query, [
317 'embeddingsEnvId' => $embeddingsEnvId,
318 'embeddingsIndex' => $embeddingsIndex,
319 'embeddingsNamespace' => $embeddingsNamespace
320 ]);
321 if ( empty( $context ) ) {
322 return null;
323 }
324 else if ( !isset( $context['content'] ) ) {
325 error_log( "AI Engine: A context without content was returned." );
326 return null;
327 }
328 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
329 $context['length'] = strlen( $context['content'] );
330 return $context;
331 }
332 #endregion
333
334 #region Users/Sessions Helpers
335
336 function get_nonce() {
337 // if ( !is_user_logged_in() ) {
338 // return null;
339 // }
340 if ( isset( $this->nonce ) ) {
341 return $this->nonce;
342 }
343 $this->nonce = wp_create_nonce( 'wp_rest' );
344 return $this->nonce;
345 }
346
347 function get_session_id() {
348 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
349 return $_COOKIE['mwai_session_id'];
350 }
351 return "N/A";
352 }
353
354 // Get the UserID from the data, or from the current user
355 function get_user_id( $data = null ) {
356 if ( isset( $data ) && isset( $data['userId'] ) ) {
357 return (int)$data['userId'];
358 }
359 if ( is_user_logged_in() ) {
360 $current_user = wp_get_current_user();
361 if ( $current_user->ID > 0 ) {
362 return $current_user->ID;
363 }
364 }
365 return null;
366 }
367
368 function get_user_data() {
369 $user = wp_get_current_user();
370 if ( empty( $user ) || empty( $user->ID ) ) {
371 return null;
372 }
373 $placeholders = array(
374 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
375 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
376 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
377 $user->data->user_login : null,
378 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
379 $user->data->display_name : null,
380 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
381 );
382 return $placeholders;
383 }
384
385 function get_ip_address( $params = null ) {
386 $ip = '127.0.0.1';
387 $headers = [
388 'HTTP_TRUE_CLIENT_IP',
389 'HTTP_CF_CONNECTING_IP',
390 'HTTP_X_REAL_IP',
391 'HTTP_CLIENT_IP',
392 'HTTP_X_FORWARDED_FOR',
393 'HTTP_X_FORWARDED',
394 'HTTP_X_CLUSTER_CLIENT_IP',
395 'HTTP_FORWARDED_FOR',
396 'HTTP_FORWARDED',
397 'REMOTE_ADDR',
398 ];
399
400 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
401 $ip = ( string )$params[ 'ip' ];
402 } else {
403 foreach ( $headers as $header ) {
404 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
405 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
406 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
407 break;
408 }
409 }
410 }
411
412 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
413 }
414
415 #endregion
416
417 #region Other Helpers
418
419 public function check_rest_nonce( $request ) {
420 $nonce = $request->get_header( 'X-WP-Nonce' );
421 return wp_verify_nonce( $nonce, 'wp_rest' );
422 }
423
424 function get_random_id( $length = 8, $excludeIds = [] ) {
425 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
426 $charactersLength = strlen( $characters );
427 $randomId = '';
428 for ( $i = 0; $i < $length; $i++ ) {
429 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
430 }
431 if ( in_array( $randomId, $excludeIds ) ) {
432 return $this->get_random_id( $length, $excludeIds );
433 }
434 return $randomId;
435 }
436
437 function is_url( $url ) {
438 return strpos( $url, 'http' ) === 0 ? true : false;
439 }
440
441 function get_post_types() {
442 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
443 $post_types = array();
444 $types = get_post_types( [], 'objects' );
445
446 // Let's get the Post Types that are enabled for Embeddings Sync
447 $embeddingsSettings = $this->get_option( 'embeddings' );
448 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
449
450 foreach ( $types as $type ) {
451 $forced = in_array( $type->name, $syncPostTypes );
452 // Should not be excluded.
453 if ( !$forced && in_array( $type->name, $excluded ) ) {
454 continue;
455 }
456 // Should be public.
457 if ( !$forced && !$type->public ) {
458 continue;
459 }
460 $post_types[] = array(
461 'name' => $type->labels->name,
462 'type' => $type->name,
463 );
464 }
465
466 // Let's get the Post Types that are enabled for Embeddings Sync
467 $embeddingsSettings = $this->get_option( 'embeddings' );
468 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
469
470 return $post_types;
471 }
472
473 function get_post( $post ) {
474 if ( is_object( $post ) ) {
475 $post = (array)$post;
476 }
477 $language = $this->get_post_language( $post['ID'] );
478 $content = $this->get_post_content( $post['ID'] );
479 $title = $post['post_title'];
480 $excerpt = $post['post_excerpt'];
481 $url = get_permalink( $post['ID'] );
482 $checksum = wp_hash( $content . $title . $url );
483 return [
484 'postId' => $post['ID'],
485 'title' => $title,
486 'content' => $content,
487 'excerpt' => $excerpt,
488 'url' => $url,
489 'language' => $language,
490 'checksum' => $checksum,
491 ];
492 }
493 #endregion
494
495 #region Usage & Costs
496
497 // Quick and dirty token estimation
498 // Let's keep this synchronized with Helpers in JS
499 static function estimate_tokens( ...$args ): int {
500 $text = "";
501 foreach ( $args as $arg ) {
502 if ( is_array( $arg ) ) {
503 foreach ( $arg as $message ) {
504 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
505 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
506 }
507 }
508 else if ( is_string( $arg ) ) {
509 $text .= $arg;
510 }
511 }
512 $averageTokenLength = 4;
513 $words = preg_split( '/\s+/', trim( $text ) );
514 $tokenCount = 0;
515 foreach ( $words as $word ) {
516 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
517 }
518 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
519 }
520
521 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0 ) {
522 if ( !is_numeric( $in_tokens ) ) {
523 throw new Exception( 'AI Engine: in_tokens must be a number.' );
524 }
525 if ( !is_numeric( $out_tokens ) ) {
526 $out_tokens = 0;
527 }
528 if ( !$model ) {
529 throw new Exception( 'AI Engine: model is required.' );
530 }
531 $usage = $this->get_option( 'openai_usage' );
532 $month = date( 'Y-m' );
533 if ( !isset( $usage[$month] ) ) {
534 $usage[$month] = array();
535 }
536 if ( !isset( $usage[$month][$model] ) ) {
537 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
538 }
539 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
540 $usage[$month][$model]['completion_tokens'] += $out_tokens;
541 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
542 $this->update_option( 'openai_usage', $usage );
543 return [
544 'prompt_tokens' => $in_tokens,
545 'completion_tokens' => $out_tokens,
546 'total_tokens' => $in_tokens + $out_tokens
547 ];
548 }
549
550 public function record_audio_usage( $model, $seconds ) {
551 if ( !is_numeric( $seconds ) ) {
552 throw new Exception( 'AI Engine: seconds must be a number.' );
553 }
554 if ( !$model ) {
555 throw new Exception( 'AI Engine: model is required.' );
556 }
557 $usage = $this->get_option( 'openai_usage' );
558 $month = date( 'Y-m' );
559 if ( !isset( $usage[$month] ) ) {
560 $usage[$month] = array();
561 }
562 if ( !isset( $usage[$month][$model] ) ) {
563 $usage[$month][$model] = array( 'seconds' => 0 );
564 }
565 $usage[$month][$model]['seconds'] += $seconds;
566 $this->update_option( 'openai_usage', $usage );
567 return [ 'seconds' => $seconds ];
568 }
569
570 public function record_images_usage( $model, $resolution, $images ) {
571 if ( !$model || !$resolution || !$images ) {
572 throw new Exception( 'Missing parameters for record_image_usage.' );
573 }
574 $usage = $this->get_option( 'openai_usage' );
575 $month = date( 'Y-m' );
576 if ( !isset( $usage[$month] ) ) {
577 $usage[$month] = array();
578 }
579 if ( !isset( $usage[$month][$model] ) ) {
580 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
581 }
582 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
583 $usage[$month][$model]['resolution'][$resolution] = 0;
584 }
585 $usage[$month][$model]['resolution'][$resolution] += $images;
586 $usage[$month][$model]['images'] += $images;
587 $this->update_option( 'openai_usage', $usage );
588 return [ 'resolution' => $resolution, 'images' => $images ];
589 }
590
591 #endregion
592
593 #region Streaming
594 public function stream_push( $data ) {
595 $out = "data: " . json_encode( $data );
596 echo $out;
597 echo "\n\n";
598 if ( ob_get_level() > 0 ) {
599 ob_end_flush();
600 }
601 flush();
602 }
603 #endregion
604
605 #region Options
606 function get_themes() {
607 $themes = get_option( $this->themes_option_name, [] );
608 $themes = empty( $themes ) ? [] : $themes;
609
610 $internalThemes = [
611 'chatgpt' => [
612 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
613 'settings' => [], 'style' => ""
614 ],
615 'messages' => [
616 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
617 'settings' => [], 'style' => ""
618 ],
619 ];
620 $customThemes = [];
621 foreach ( $themes as $theme ) {
622 if ( isset( $internalThemes[$theme['themeId']] ) ) {
623 $internalThemes[$theme['themeId']] = $theme;
624 continue;
625 }
626 $customThemes[] = $theme;
627 }
628 return array_merge(array_values($internalThemes), $customThemes);
629 }
630
631 function update_themes( $themes ) {
632 update_option( $this->themes_option_name, $themes );
633 return $themes;
634 }
635
636 function get_chatbots() {
637 $chatbots = get_option( $this->chatbots_option_name, [] );
638 $hasChanges = false;
639 if ( empty( $chatbots ) ) {
640 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
641 }
642 foreach ( $chatbots as &$chatbot ) {
643 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
644 // Use default value if not set.
645 if ( !isset( $chatbot[$key] ) ) {
646 $chatbot[$key] = $value;
647 }
648 }
649 // After September 2023, let's remove this if statement.
650 // if ( isset( $chatbot['chatId'] ) ) {
651 // $chatbot['botId'] = $chatbot['chatId'];
652 // unset( $chatbot['chatId'] );
653 // $hasChanges = true;
654 // }
655 // After September 2023, let's remove this if statement.
656 // if ( empty( $chatbot['botId'] ) && $chatbot['name'] === 'default' ) {
657 // $chatbot['botId'] = sanitize_title( $chatbot['name'] );
658 // $hasChanges = true;
659 // }
660 }
661 if ( $hasChanges ) {
662 update_option( $this->chatbots_option_name, $chatbots );
663 }
664 return $chatbots;
665 }
666
667 function get_chatbot( $botId ) {
668 $chatbots = $this->get_chatbots();
669 foreach ( $chatbots as $chatbot ) {
670 if ( $chatbot['botId'] === (string)$botId ) {
671 return $chatbot;
672 }
673 }
674 return null;
675 }
676
677 function get_embeddings_env( $envId ) {
678 $envs = $this->get_option( 'embeddings_envs' );
679 foreach ( $envs as $env ) {
680 if ( $env['id'] === $envId ) {
681 return $env;
682 }
683 }
684 return null;
685 }
686
687 function get_ai_env( $envId ) {
688 $envs = $this->get_option( 'ai_envs' );
689 foreach ( $envs as $env ) {
690 if ( $env['id'] === $envId ) {
691 return $env;
692 }
693 }
694 return null;
695 }
696
697 function get_assistant( $envId, $assistantId ) {
698 $env = $this->get_ai_env( $envId );
699 if ( !$env ) {
700 return null;
701 }
702 $assistants = $env['assistants'];
703 foreach ( $assistants as $assistant ) {
704 if ( $assistant['id'] === $assistantId ) {
705 return $assistant;
706 }
707 }
708 return null;
709 }
710
711 function get_theme( $themeId ) {
712 $themes = $this->get_themes();
713 foreach ( $themes as $theme ) {
714 if ( $theme['themeId'] === $themeId ) {
715 return $theme;
716 }
717 }
718 return null;
719 }
720
721 function update_chatbots( $chatbots ) {
722 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
723 $whiteSpacedFields = [ 'context' ];
724 foreach ( $chatbots as &$chatbot ) {
725 foreach ( $chatbot as $key => &$value ) {
726 if ( in_array( $key, $htmlFields ) ) {
727 $value = wp_kses_post( $value );
728 }
729 else if ( in_array( $key, $whiteSpacedFields ) ) {
730 $value = sanitize_textarea_field( $value );
731 }
732 else {
733 $value = sanitize_text_field( $value );
734 }
735 }
736 }
737
738 update_option( $this->chatbots_option_name, $chatbots );
739 return $chatbots;
740 }
741
742 function get_all_options( $force = false ) {
743 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
744 // That causes issues with the mwai_languages filter.
745 // if ( !$force && !is_null( $this->options ) ) {
746 // return $this->options;
747 // }
748 $options = get_option( $this->option_name, [] );
749 $options = $this->sanitize_options( $options );
750 foreach ( MWAI_OPTIONS as $key => $value ) {
751 if ( !isset( $options[$key] ) ) {
752 $options[$key] = $value;
753 }
754 if ( $key === 'languages' ) {
755 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
756 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
757 }
758 }
759 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
760 $options['default_limits'] = MWAI_LIMITS;
761 $options['openai_models'] = apply_filters( 'mwai_openai_models', Meow_MWAI_Engines_OpenAI::get_models_static() );
762 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
763
764 //$this->options = $options;
765 return $options;
766 }
767
768 // Sanitize options when we update the plugi or perform some updates
769 // if we change the structure of the options.
770 function sanitize_options( $options ) {
771 $needs_update = false;
772
773 // This list was updated on December 11, 2023. After May 2024, let's remove this.
774 $old_options = [
775 'shortcode_chat_default_params',
776 'shortcode_chat_params_override',
777 'module_legacy_finetunes',
778 'shortcode_chat_legacy',
779 'shortcode_chat_inject',
780 'shortcode_chat_styles',
781 'dynamic_max_tokens',
782 'shortcode_chat_formatting',
783 'shortcode_forms_legacy',
784 ];
785 foreach ( $old_options as $old_option ) {
786 if ( isset( $options[$old_option] ) ) {
787 unset( $options[$old_option] );
788 $needs_update = true;
789 }
790 }
791
792 // This upgrades namespace to multi-namespaces (June 2023)
793 // After January 2024, let's remove this.
794 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
795 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
796 unset( $options['pinecone']['namespace'] );
797 $needs_update = true;
798 }
799 // Support for Multi Vector DB Environments
800 // After June 2024, let's remove this.
801 if ( !isset( $options['embeddings_envs'] ) ) {
802 $options['embeddings_envs'] = [];
803 $default_id = $this->get_random_id();
804 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
805 $options['embeddings_envs'][] = [
806 'id' => $default_id,
807 'name' => 'Pinecone',
808 'type' => 'pinecone',
809 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
810 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
811 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
812 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
813 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
814 ];
815 $options['embeddings_default_env'] = $default_id;
816 $needs_update = true;
817 }
818 if ( isset( $options['pinecone'] ) ) {
819 unset( $options['pinecone'] );
820 $needs_update = true;
821 }
822 // Support for Multi AI Environments
823 // After June 2024, let's remove this.
824 if ( !isset( $options['ai_envs'] ) ) {
825 $options['ai_envs'] = [];
826 $default_openai_id = $this->get_random_id();
827 $default_azure_id = $this->get_random_id();
828 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
829 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
830 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
831
832 // OpenAI
833 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
834 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
835 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
836 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
837 $options['openai_finetunes_deleted'] : [];
838 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
839 $options['openai_legacy_finetunes'] : [];
840 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
841 $options['openai_legacy_finetunes_deleted'] : [];
842 $options['ai_envs'][] = [
843 'id' => $default_openai_id,
844 'name' => 'OpenAI',
845 'type' => 'openai',
846 'apikey' => $openai_apikey,
847 'finetunes' => $openai_finetunes,
848 'finetunes_deleted' => $openai_finetunes_deleted,
849 'legacy_finetunes' => $openai_legacy_finetunes,
850 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
851 ];
852 }
853
854 // Azure
855 if ( !empty( $azure_endpoint ) ) {
856 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
857 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
858 $options['ai_envs'][] = [
859 'id' => $default_azure_id,
860 'name' => 'Azure',
861 'type' => 'azure',
862 'apikey' => $azure_apikey,
863 'endpoint' => $azure_endpoint,
864 'deployments' => $azure_deployments,
865 ];
866 }
867
868 $options['ai_default_env'] = $default_openai_id;
869 if ( $openai_service === 'azure' ) {
870 $options['ai_default_env'] = $default_azure_id;
871 }
872 $needs_update = true;
873 }
874 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
875 unset( $options['openai_apikey'] );
876 unset( $options['openai_finetunes'] );
877 unset( $options['openai_finetunes_deleted'] );
878 unset( $options['openai_legacy_finetunes'] );
879 unset( $options['openai_legacy_finetunes_deleted'] );
880 unset( $options['openai_azure_apikey'] );
881 unset( $options['openai_azure_endpoint'] );
882 unset( $options['openai_azure_deployments'] );
883 unset( $options['openai_service'] );
884 $needs_update = true;
885 }
886
887 // The IDs for the embeddings environments are generated here.
888 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
889 $embeddings_default_exists = false;
890 if ( isset( $options['embeddings_envs'] ) ) {
891 foreach ( $options['embeddings_envs'] as &$env ) {
892 if ( !isset( $env['id'] ) ) {
893 $env['id'] = $this->get_random_id();
894 $needs_update = true;
895 }
896 if ( $env['id'] === $options['embeddings_default_env'] ) {
897 $embeddings_default_exists = true;
898 }
899 }
900 }
901 if ( !$embeddings_default_exists ) {
902 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
903 $needs_update = true;
904 }
905
906 // The IDs for the AI environments are generated here.
907 $ai_default_exists = false;
908 if ( isset( $options['ai_envs'] ) ) {
909 foreach ( $options['ai_envs'] as &$env ) {
910 if ( !isset( $env['id'] ) ) {
911 $env['id'] = $this->get_random_id();
912 $needs_update = true;
913 }
914 if ( $env['id'] === $options['ai_default_env'] ) {
915 $ai_default_exists = true;
916 }
917 }
918 }
919 if ( !$ai_default_exists ) {
920 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
921 $needs_update = true;
922 }
923
924 if ( $needs_update ) {
925 update_option( $this->option_name, $options, false );
926 }
927
928 return $options;
929 }
930
931 function update_options( $options ) {
932 if ( !update_option( $this->option_name, $options, false ) ) {
933 return false;
934 }
935 $options = $this->get_all_options( true );
936 return $options;
937 }
938
939 function update_option( $option, $value ) {
940 $options = $this->get_all_options( true );
941 $options[$option] = $value;
942 return $this->update_options( $options );
943 }
944
945 function get_option( $option, $default = null ) {
946 $options = $this->get_all_options();
947 return $options[$option] ?? $default;
948 }
949
950 function update_ai_env( $env_id, $option, $value ) {
951 $options = $this->get_all_options( true );
952 foreach ( $options['ai_envs'] as &$env ) {
953 if ( $env['id'] === $env_id ) {
954 $env[$option] = $value;
955 break;
956 }
957 }
958 return $this->update_options( $options );
959 }
960
961 function reset_options() {
962 delete_option( $this->themes_option_name );
963 delete_option( $this->chatbots_option_name );
964 delete_option( $this->option_name );
965 return $this->get_all_options( true );
966 }
967 #endregion
968 }
969
970 ?>