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