PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.1.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.1.0
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
920 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_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 $image_data = $this->download_image( $url );
222 if ( !$image_data ) {
223 throw new Exception( 'Could not download the image.' );
224 }
225 $upload_dir = wp_upload_dir();
226 if ( empty( $filename ) ) {
227 $filename = basename( $url );
228 $filename = sanitize_file_name( $filename );
229 if ( strlen( $filename ) > 32 ) {
230 $filename = $this->get_random_id( 16 ) . '.jpg';
231 }
232 if ( strpos( $filename, '.' ) === false ) {
233 $filename .= '.jpg';
234 }
235 }
236 $wp_filetype = wp_check_filetype( $filename );
237 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
238 $file = $upload_dir['path'] . '/' . $filename;
239 }
240 else {
241 $file = $upload_dir['basedir'] . '/' . $filename;
242 }
243
244 // Make sure the file is unique, if not, add a number to the end of the file before the extension
245 $i = 1;
246 $parts = pathinfo( $file );
247 while ( file_exists( $file ) ) {
248 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
249 $i++;
250 }
251
252 // Write the file
253 file_put_contents( $file, $image_data );
254 $attachment = [
255 'post_mime_type' => $wp_filetype['type'],
256 'post_title' => $title ?? '',
257 'post_content' => $description ?? '',
258 'post_excerpt' => $caption ?? '',
259 'post_status' => 'inherit'
260 ];
261 // Register the file as a Media Library attachment
262 $attachmentId = wp_insert_attachment( $attachment, $file );
263 require_once( ABSPATH . 'wp-admin/includes/image.php' );
264 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
265 wp_update_attachment_metadata( $attachmentId, $attachment_data );
266 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
267 return $attachmentId;
268 }
269 #endregion
270
271 #region Context-Related Helpers
272 function retrieve_context( $params, $query ) {
273 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
274 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
275 $embeddingsIndex = $params['embeddingsIndex'] ?? null;
276 $embeddingsNamespace = $params['embeddingsNamespace'] ?? null;
277 $context = apply_filters( 'mwai_context_search', [], $query, [
278 'embeddingsEnvId' => $embeddingsEnvId,
279 'embeddingsIndex' => $embeddingsIndex,
280 'embeddingsNamespace' => $embeddingsNamespace
281 ]);
282 if ( empty( $context ) ) {
283 return null;
284 }
285 else if ( !isset( $context['content'] ) ) {
286 error_log( "AI Engine: A context without content was returned." );
287 return null;
288 }
289 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
290 $context['length'] = strlen( $context['content'] );
291 return $context;
292 }
293 #endregion
294
295 #region Users/Sessions Helpers
296
297 function get_nonce() {
298 // if ( !is_user_logged_in() ) {
299 // return null;
300 // }
301 if ( isset( $this->nonce ) ) {
302 return $this->nonce;
303 }
304 $this->nonce = wp_create_nonce( 'wp_rest' );
305 return $this->nonce;
306 }
307
308 function get_session_id() {
309 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
310 return $_COOKIE['mwai_session_id'];
311 }
312 return "N/A";
313 }
314
315 // Get the UserID from the data, or from the current user
316 function get_user_id( $data = null ) {
317 if ( isset( $data ) && isset( $data['userId'] ) ) {
318 return (int)$data['userId'];
319 }
320 if ( is_user_logged_in() ) {
321 $current_user = wp_get_current_user();
322 if ( $current_user->ID > 0 ) {
323 return $current_user->ID;
324 }
325 }
326 return null;
327 }
328
329 function get_user_data() {
330 $user = wp_get_current_user();
331 if ( empty( $user ) || empty( $user->ID ) ) {
332 return null;
333 }
334 $placeholders = array(
335 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
336 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
337 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
338 $user->data->user_login : null,
339 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
340 $user->data->display_name : null,
341 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
342 );
343 return $placeholders;
344 }
345
346 function get_ip_address( $params = null ) {
347 $ip = '127.0.0.1';
348 $headers = [
349 'HTTP_TRUE_CLIENT_IP',
350 'HTTP_CF_CONNECTING_IP',
351 'HTTP_X_REAL_IP',
352 'HTTP_CLIENT_IP',
353 'HTTP_X_FORWARDED_FOR',
354 'HTTP_X_FORWARDED',
355 'HTTP_X_CLUSTER_CLIENT_IP',
356 'HTTP_FORWARDED_FOR',
357 'HTTP_FORWARDED',
358 'REMOTE_ADDR',
359 ];
360
361 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
362 $ip = ( string )$params[ 'ip' ];
363 } else {
364 foreach ( $headers as $header ) {
365 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
366 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
367 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
368 break;
369 }
370 }
371 }
372
373 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
374 }
375
376 #endregion
377
378 #region Other Helpers
379
380 public function check_rest_nonce( $request ) {
381 $nonce = $request->get_header( 'X-WP-Nonce' );
382 return wp_verify_nonce( $nonce, 'wp_rest' );
383 }
384
385 function get_random_id( $length = 8, $excludeIds = [] ) {
386 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
387 $charactersLength = strlen( $characters );
388 $randomId = '';
389 for ( $i = 0; $i < $length; $i++ ) {
390 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
391 }
392 if ( in_array( $randomId, $excludeIds ) ) {
393 return $this->get_random_id( $length, $excludeIds );
394 }
395 return $randomId;
396 }
397
398 function is_url( $url ) {
399 return strpos( $url, 'http' ) === 0 ? true : false;
400 }
401
402 function get_post_types() {
403 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
404 $post_types = array();
405 $types = get_post_types( [], 'objects' );
406
407 // Let's get the Post Types that are enabled for Embeddings Sync
408 $embeddingsSettings = $this->get_option( 'embeddings' );
409 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
410
411 foreach ( $types as $type ) {
412 $forced = in_array( $type->name, $syncPostTypes );
413 // Should not be excluded.
414 if ( !$forced && in_array( $type->name, $excluded ) ) {
415 continue;
416 }
417 // Should be public.
418 if ( !$forced && !$type->public ) {
419 continue;
420 }
421 $post_types[] = array(
422 'name' => $type->labels->name,
423 'type' => $type->name,
424 );
425 }
426
427 // Let's get the Post Types that are enabled for Embeddings Sync
428 $embeddingsSettings = $this->get_option( 'embeddings' );
429 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
430
431 return $post_types;
432 }
433
434 function get_post( $post ) {
435 if ( is_object( $post ) ) {
436 $post = (array)$post;
437 }
438 $language = $this->get_post_language( $post['ID'] );
439 $content = $this->get_post_content( $post['ID'] );
440 $title = $post['post_title'];
441 $excerpt = $post['post_excerpt'];
442 $url = get_permalink( $post['ID'] );
443 $checksum = wp_hash( $content . $title . $url );
444 return [
445 'postId' => $post['ID'],
446 'title' => $title,
447 'content' => $content,
448 'excerpt' => $excerpt,
449 'url' => $url,
450 'language' => $language,
451 'checksum' => $checksum,
452 ];
453 }
454 #endregion
455
456 #region Usage & Costs
457
458 // Quick and dirty token estimation
459 // Let's keep this synchronized with Helpers in JS
460 static function estimate_tokens( ...$args ): int {
461 $text = "";
462 foreach ( $args as $arg ) {
463 if ( is_array( $arg ) ) {
464 foreach ( $arg as $message ) {
465 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
466 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
467 }
468 }
469 else if ( is_string( $arg ) ) {
470 $text .= $arg;
471 }
472 }
473 $averageTokenLength = 4;
474 $words = preg_split( '/\s+/', trim( $text ) );
475 $tokenCount = 0;
476 foreach ( $words as $word ) {
477 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
478 }
479 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
480 }
481
482 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0 ) {
483 if ( !is_numeric( $in_tokens ) ) {
484 throw new Exception( 'AI Engine: in_tokens must be a number.' );
485 }
486 if ( !is_numeric( $out_tokens ) ) {
487 $out_tokens = 0;
488 }
489 if ( !$model ) {
490 throw new Exception( 'AI Engine: model is required.' );
491 }
492 $usage = $this->get_option( 'openai_usage' );
493 $month = date( 'Y-m' );
494 if ( !isset( $usage[$month] ) ) {
495 $usage[$month] = array();
496 }
497 if ( !isset( $usage[$month][$model] ) ) {
498 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
499 }
500 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
501 $usage[$month][$model]['completion_tokens'] += $out_tokens;
502 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
503 $this->update_option( 'openai_usage', $usage );
504 return [
505 'prompt_tokens' => $in_tokens,
506 'completion_tokens' => $out_tokens,
507 'total_tokens' => $in_tokens + $out_tokens
508 ];
509 }
510
511 public function record_audio_usage( $model, $seconds ) {
512 if ( !is_numeric( $seconds ) ) {
513 throw new Exception( 'AI Engine: seconds must be a number.' );
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( 'seconds' => 0 );
525 }
526 $usage[$month][$model]['seconds'] += $seconds;
527 $this->update_option( 'openai_usage', $usage );
528 return [ 'seconds' => $seconds ];
529 }
530
531 public function record_images_usage( $model, $resolution, $images ) {
532 if ( !$model || !$resolution || !$images ) {
533 throw new Exception( 'Missing parameters for record_image_usage.' );
534 }
535 $usage = $this->get_option( 'openai_usage' );
536 $month = date( 'Y-m' );
537 if ( !isset( $usage[$month] ) ) {
538 $usage[$month] = array();
539 }
540 if ( !isset( $usage[$month][$model] ) ) {
541 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
542 }
543 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
544 $usage[$month][$model]['resolution'][$resolution] = 0;
545 }
546 $usage[$month][$model]['resolution'][$resolution] += $images;
547 $usage[$month][$model]['images'] += $images;
548 $this->update_option( 'openai_usage', $usage );
549 return [ 'resolution' => $resolution, 'images' => $images ];
550 }
551
552 #endregion
553
554 #region Streaming
555 public function stream_push( $data ) {
556 $out = "data: " . json_encode( $data );
557 echo $out;
558 echo "\n\n";
559 if ( ob_get_level() > 0 ) {
560 ob_end_flush();
561 }
562 flush();
563 }
564 #endregion
565
566 #region Options
567 function get_themes() {
568 $themes = get_option( $this->themes_option_name, [] );
569 $themes = empty( $themes ) ? [] : $themes;
570
571 $internalThemes = [
572 'chatgpt' => [
573 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
574 'settings' => [], 'style' => ""
575 ],
576 'messages' => [
577 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
578 'settings' => [], 'style' => ""
579 ],
580 ];
581 $customThemes = [];
582 foreach ( $themes as $theme ) {
583 if ( isset( $internalThemes[$theme['themeId']] ) ) {
584 $internalThemes[$theme['themeId']] = $theme;
585 continue;
586 }
587 $customThemes[] = $theme;
588 }
589 return array_merge(array_values($internalThemes), $customThemes);
590 }
591
592 function update_themes( $themes ) {
593 update_option( $this->themes_option_name, $themes );
594 return $themes;
595 }
596
597 function get_chatbots() {
598 $chatbots = get_option( $this->chatbots_option_name, [] );
599 $hasChanges = false;
600 if ( empty( $chatbots ) ) {
601 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
602 }
603 foreach ( $chatbots as &$chatbot ) {
604 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
605 // Use default value if not set.
606 if ( !isset( $chatbot[$key] ) ) {
607 $chatbot[$key] = $value;
608 }
609 }
610 // After September 2023, let's remove this if statement.
611 // if ( isset( $chatbot['chatId'] ) ) {
612 // $chatbot['botId'] = $chatbot['chatId'];
613 // unset( $chatbot['chatId'] );
614 // $hasChanges = true;
615 // }
616 // After September 2023, let's remove this if statement.
617 // if ( empty( $chatbot['botId'] ) && $chatbot['name'] === 'default' ) {
618 // $chatbot['botId'] = sanitize_title( $chatbot['name'] );
619 // $hasChanges = true;
620 // }
621 }
622 if ( $hasChanges ) {
623 update_option( $this->chatbots_option_name, $chatbots );
624 }
625 return $chatbots;
626 }
627
628 function get_chatbot( $botId ) {
629 $chatbots = $this->get_chatbots();
630 foreach ( $chatbots as $chatbot ) {
631 if ( $chatbot['botId'] === (string)$botId ) {
632 return $chatbot;
633 }
634 }
635 return null;
636 }
637
638 function get_environment( $envId ) {
639 $envs = $this->get_option( 'ai_envs' );
640 foreach ( $envs as $env ) {
641 if ( $env['id'] === $envId ) {
642 return $env;
643 }
644 }
645 return null;
646 }
647
648 function get_assistant( $envId, $assistantId ) {
649 $env = $this->get_environment( $envId );
650 if ( !$env ) {
651 return null;
652 }
653 $assistants = $env['assistants'];
654 foreach ( $assistants as $assistant ) {
655 if ( $assistant['id'] === $assistantId ) {
656 return $assistant;
657 }
658 }
659 return null;
660 }
661
662 function get_theme( $themeId ) {
663 $themes = $this->get_themes();
664 foreach ( $themes as $theme ) {
665 if ( $theme['themeId'] === $themeId ) {
666 return $theme;
667 }
668 }
669 return null;
670 }
671
672 function update_chatbots( $chatbots ) {
673 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
674 $whiteSpacedFields = [ 'context' ];
675 foreach ( $chatbots as &$chatbot ) {
676 foreach ( $chatbot as $key => &$value ) {
677 if ( in_array( $key, $htmlFields ) ) {
678 $value = wp_kses_post( $value );
679 }
680 else if ( in_array( $key, $whiteSpacedFields ) ) {
681 $value = sanitize_textarea_field( $value );
682 }
683 else {
684 $value = sanitize_text_field( $value );
685 }
686 }
687 }
688
689 update_option( $this->chatbots_option_name, $chatbots );
690 return $chatbots;
691 }
692
693 function get_all_options( $force = false ) {
694 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
695 // That causes issues with the mwai_languages filter.
696 // if ( !$force && !is_null( $this->options ) ) {
697 // return $this->options;
698 // }
699 $options = get_option( $this->option_name, [] );
700 $options = $this->sanitize_options( $options );
701 foreach ( MWAI_OPTIONS as $key => $value ) {
702 if ( !isset( $options[$key] ) ) {
703 $options[$key] = $value;
704 }
705 if ( $key === 'languages' ) {
706 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
707 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
708 }
709 }
710 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
711 $options['default_limits'] = MWAI_LIMITS;
712 $options['openai_models'] = Meow_MWAI_Engines_OpenAI::get_models_static();
713 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
714
715 //$this->options = $options;
716 return $options;
717 }
718
719 // Sanitize options when we update the plugi or perform some updates
720 // if we change the structure of the options.
721 function sanitize_options( $options ) {
722 $needs_update = false;
723
724 // This list was updated on December 11, 2023. After May 2024, let's remove this.
725 $old_options = [
726 'shortcode_chat_default_params',
727 'shortcode_chat_params_override',
728 'module_legacy_finetunes',
729 'shortcode_chat_legacy',
730 'shortcode_chat_inject',
731 'shortcode_chat_styles',
732 'dynamic_max_tokens',
733 'shortcode_chat_formatting',
734 'shortcode_forms_legacy',
735 ];
736 foreach ( $old_options as $old_option ) {
737 if ( isset( $options[$old_option] ) ) {
738 unset( $options[$old_option] );
739 $needs_update = true;
740 }
741 }
742
743 // This upgrades namespace to multi-namespaces (June 2023)
744 // After January 2024, let's remove this.
745 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
746 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
747 unset( $options['pinecone']['namespace'] );
748 $needs_update = true;
749 }
750 // Support for Multi Vector DB Environments
751 // After June 2024, let's remove this.
752 if ( !isset( $options['embeddings_envs'] ) ) {
753 $options['embeddings_envs'] = [];
754 $default_id = $this->get_random_id();
755 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
756 $options['embeddings_envs'][] = [
757 'id' => $default_id,
758 'name' => 'Pinecone',
759 'type' => 'pinecone',
760 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
761 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
762 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
763 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
764 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
765 ];
766 $options['embeddings_default_env'] = $default_id;
767 $needs_update = true;
768 }
769 if ( isset( $options['pinecone'] ) ) {
770 unset( $options['pinecone'] );
771 $needs_update = true;
772 }
773 // Support for Multi AI Environments
774 // After June 2024, let's remove this.
775 if ( !isset( $options['ai_envs'] ) ) {
776 $options['ai_envs'] = [];
777 $default_openai_id = $this->get_random_id();
778 $default_azure_id = $this->get_random_id();
779 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
780 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
781 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
782
783 // OpenAI
784 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
785 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
786 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
787 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
788 $options['openai_finetunes_deleted'] : [];
789 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
790 $options['openai_legacy_finetunes'] : [];
791 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
792 $options['openai_legacy_finetunes_deleted'] : [];
793 $options['ai_envs'][] = [
794 'id' => $default_openai_id,
795 'name' => 'OpenAI',
796 'type' => 'openai',
797 'apikey' => $openai_apikey,
798 'finetunes' => $openai_finetunes,
799 'finetunes_deleted' => $openai_finetunes_deleted,
800 'legacy_finetunes' => $openai_legacy_finetunes,
801 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
802 ];
803 }
804
805 // Azure
806 if ( !empty( $azure_endpoint ) ) {
807 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
808 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
809 $options['ai_envs'][] = [
810 'id' => $default_azure_id,
811 'name' => 'Azure',
812 'type' => 'azure',
813 'apikey' => $azure_apikey,
814 'endpoint' => $azure_endpoint,
815 'deployments' => $azure_deployments,
816 ];
817 }
818
819 $options['ai_default_env'] = $default_openai_id;
820 if ( $openai_service === 'azure' ) {
821 $options['ai_default_env'] = $default_azure_id;
822 }
823 $needs_update = true;
824 }
825 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
826 unset( $options['openai_apikey'] );
827 unset( $options['openai_finetunes'] );
828 unset( $options['openai_finetunes_deleted'] );
829 unset( $options['openai_legacy_finetunes'] );
830 unset( $options['openai_legacy_finetunes_deleted'] );
831 unset( $options['openai_azure_apikey'] );
832 unset( $options['openai_azure_endpoint'] );
833 unset( $options['openai_azure_deployments'] );
834 unset( $options['openai_service'] );
835 $needs_update = true;
836 }
837
838 // The IDs for the embeddings environments are generated here.
839 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
840 $embeddings_default_exists = false;
841 if ( isset( $options['embeddings_envs'] ) ) {
842 foreach ( $options['embeddings_envs'] as &$env ) {
843 if ( !isset( $env['id'] ) ) {
844 $env['id'] = $this->get_random_id();
845 $needs_update = true;
846 }
847 if ( $env['id'] === $options['embeddings_default_env'] ) {
848 $embeddings_default_exists = true;
849 }
850 }
851 }
852 if ( !$embeddings_default_exists ) {
853 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
854 $needs_update = true;
855 }
856
857 // The IDs for the AI environments are generated here.
858 $ai_default_exists = false;
859 if ( isset( $options['ai_envs'] ) ) {
860 foreach ( $options['ai_envs'] as &$env ) {
861 if ( !isset( $env['id'] ) ) {
862 $env['id'] = $this->get_random_id();
863 $needs_update = true;
864 }
865 if ( $env['id'] === $options['ai_default_env'] ) {
866 $ai_default_exists = true;
867 }
868 }
869 }
870 if ( !$ai_default_exists ) {
871 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
872 $needs_update = true;
873 }
874
875 if ( $needs_update ) {
876 update_option( $this->option_name, $options, false );
877 }
878
879 return $options;
880 }
881
882 function update_options( $options ) {
883 if ( !update_option( $this->option_name, $options, false ) ) {
884 return false;
885 }
886 $options = $this->get_all_options( true );
887 return $options;
888 }
889
890 function update_option( $option, $value ) {
891 $options = $this->get_all_options( true );
892 $options[$option] = $value;
893 return $this->update_options( $options );
894 }
895
896 function get_option( $option, $default = null ) {
897 $options = $this->get_all_options();
898 return $options[$option] ?? $default;
899 }
900
901 function update_ai_env( $env_id, $option, $value ) {
902 $options = $this->get_all_options( true );
903 foreach ( $options['ai_envs'] as &$env ) {
904 if ( $env['id'] === $env_id ) {
905 $env[$option] = $value;
906 break;
907 }
908 }
909 return $this->update_options( $options );
910 }
911
912 function reset_options() {
913 //return $this->update_options( MWAI_OPTIONS );
914 delete_option( $this->option_name );
915 return $this->get_all_options( true );
916 }
917 #endregion
918 }
919
920 ?>