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