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