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