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