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