PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.6.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.6.3
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 1 year ago modules 1 year ago queries 1 year ago admin.php 2 years ago api.php 1 year ago core.php 1 year ago init.php 2 years ago logging.php 1 year ago reply.php 1 year ago rest.php 1 year ago
core.php
1321 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 public $tasks = null;
20 public $magicWand = null;
21 private $options = null;
22 private $option_name = 'mwai_options';
23 private $themes_option_name = 'mwai_themes';
24 private $chatbots_option_name = 'mwai_chatbots';
25 private $nonce = null;
26
27 public $chatbot = null;
28 public $discussions = null;
29
30 public function __construct() {
31 Meow_MWAI_Logging::init( 'mwai_options', 'AI Engine' );
32 $this->site_url = get_site_url();
33 $this->is_rest = MeowCommon_Helpers::is_rest();
34 $this->is_cli = defined( 'WP_CLI' );
35 $this->files = new Meow_MWAI_Modules_Files( $this );
36 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
37
38 add_action( 'plugins_loaded', array( $this, 'init' ) );
39 add_action( 'wp_register_script', array( $this, 'register_scripts' ) );
40 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
41 add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
42 }
43
44 #region Init & Scripts
45 function init() {
46 global $mwai;
47 $this->chatbot = null;
48 $this->discussions = null;
49 new Meow_MWAI_Modules_Security( $this );
50
51 // REST API
52 if ( $this->is_rest ) {
53 new Meow_MWAI_Rest( $this );
54 }
55
56 // WP Admin
57 if ( is_admin() ) {
58 new Meow_MWAI_Admin( $this );
59 }
60
61 // GDPR Module
62 if ( $this->get_option( 'chatbot_gdpr_consent' ) ) {
63 new Meow_MWAI_Modules_GDPR( $this );
64 }
65
66 // Suggestions Module
67 if ( $this->get_option( 'module_suggestions' ) && ( is_admin() || $this->is_rest ) ) {
68 $this->magicWand = new Meow_MWAI_Modules_Wand( $this );
69 }
70
71 // Administrator in WP Admin
72 if ( is_admin() && current_user_can( 'manage_options' ) ) {
73 $module_advisor = $this->get_option( 'module_advisor' );
74 if ( $module_advisor ) {
75 new Meow_MWAI_Modules_Advisor( $this );
76 }
77 }
78
79 // Chatbots & Discussions
80 if ( $this->get_option( 'module_chatbots' ) ) {
81 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
82 $this->discussions = new Meow_MWAI_Modules_Discussions();
83 }
84
85 // Advanced Core
86 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
87 new MeowPro_MWAI_Core( $this );
88 }
89
90 // Simple API
91 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
92 }
93
94 public function register_scripts() {
95 // Register Highlight.js
96 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
97 // Register CSS for the themes
98 $themes = $this->get_themes();
99 foreach ( $themes as $theme ) {
100 if ( $theme['type'] === 'internal' ) {
101 $themeId = $theme['themeId'];
102 $filename = $themeId . '.css';
103 $physical_file = trailingslashit( MWAI_PATH ) . 'themes/' . $filename;
104 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
105 wp_register_style( 'mwai_chatbot_theme_' . $themeId, trailingslashit( MWAI_URL )
106 . 'themes/' . $filename, [], $cache_buster );
107 }
108 }
109 }
110
111 public function enqueue_themes() {
112 // TODO: We should optimize and only load the themes that are used.
113 $themes = $this->get_themes();
114 foreach ( $themes as $theme ) {
115 if ( $theme['type'] === 'internal' ) {
116 $themeId = $theme['themeId'];
117 wp_enqueue_style( "mwai_chatbot_theme_$themeId" );
118 }
119 }
120 }
121
122 #endregion
123
124 #region Roles & Capabilities
125 function can_start_session() {
126 return apply_filters( 'mwai_allow_session', true );
127 }
128
129 function can_access_settings() {
130 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
131 }
132
133 function can_access_features() {
134 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
135 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
136 }
137
138 function can_access_public_api( $feature, $extra ) {
139 $logged_in = is_user_logged_in();
140 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
141 }
142
143 #endregion
144
145 #region AI-Related Helpers
146 function run_query( $query, $streamCallback = null, $markdown = false ) {
147 $envId = !empty( $query->envId ) ? $query->envId : null;
148 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
149
150 // If the engine is not set, we need to set it to the default one.
151 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
152 if ( $query instanceof Meow_MWAI_Query_Text ) {
153 $this->set_if_empty_defaults( $query, 'ai_default_env', 'ai_default_model' );
154 }
155 if ( $query instanceof Meow_MWAI_Query_Embed ) {
156 $this->set_if_empty_defaults( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
157 }
158 else if ( $query instanceof Meow_MWAI_Query_Image ) {
159 $this->set_if_empty_defaults( $query, 'ai_images_default_env', 'ai_images_default_model' );
160 }
161 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
162 $this->set_if_empty_defaults( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
163 }
164 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
165 }
166
167 // Let's run the query.
168 $reply = $engine->run( $query, $streamCallback );
169
170 // Let's allow to modify the reply before it is sent.
171 if ( $markdown ) {
172 if ( $query instanceof Meow_MWAI_Query_Image ) {
173 $reply->result = "";
174 foreach ( $reply->results as $result ) {
175 $reply->result .= "![Image]($result)\n";
176 }
177 }
178 }
179
180 return $reply;
181 }
182
183 private function set_if_empty_defaults( $query, $envOption, $modelOption ) {
184 $defaultEnv = $this->get_option( $envOption );
185 $defaultModel = $this->get_option( $modelOption );
186 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
187 throw new Exception( 'AI Engine: The default environment and model are not set.' );
188 }
189 $query->set_env_id( $defaultEnv );
190 $query->set_model( $defaultModel );
191 }
192 #endregion
193
194 #region Text-Related Helpers
195
196 // Clean the text perfectly, resolve shortcodes, etc, etc.
197 function clean_text( $rawText = "" ) {
198 $text = html_entity_decode( $rawText );
199 $text = wp_strip_all_tags( $text );
200 $text = preg_replace( '/[\r\n]+/', "\n", $text );
201 $text = preg_replace( '/\n+/', "\n", $text );
202 $text = preg_replace( '/\t+/', "\t", $text );
203 return $text . " ";
204 }
205
206 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
207 function clean_sentences( $text, $maxLength = null ) {
208 // Step 1: Identify URLs and replace them with a placeholder.
209 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
210 preg_match_all($urlPattern, $text, $urls);
211 $urlPlaceholders = array();
212 foreach ($urls[0] as $index => $url) {
213 $placeholder = "{urlPlaceholder" . $index . "}";
214 $text = str_replace($url, $placeholder, $text);
215 $urlPlaceholders[$placeholder] = $url;
216 }
217
218 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
219 $sentences = preg_split('/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
220 $hashes = array();
221 $uniqueSentences = array();
222 $total = 0;
223
224 foreach ( $sentences as $sentence ) {
225 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
226 $hash = md5( $sentence );
227 if ( !in_array( $hash, $hashes ) ) {
228 $length = mb_strlen( $sentence, 'UTF-8' );
229 if ( $total + $length > $maxLength ) {
230 continue;
231 }
232 $hashes[] = $hash;
233 $uniqueSentences[] = $sentence;
234 $total += $length;
235 }
236 }
237
238 $freshText = implode( " ", $uniqueSentences );
239
240 // Step 3: Restore URLs in the final text.
241 foreach ($urlPlaceholders as $placeholder => $url) {
242 $freshText = str_replace($placeholder, $url, $freshText);
243 }
244
245 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
246 return $freshText;
247 }
248
249 function get_post_content( $postId ) {
250 $post = get_post( $postId );
251 if ( !$post ) {
252 return false;
253 }
254 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
255 $pattern = '/\[mwai_.*?\]/';
256 $text = preg_replace( $pattern, '', $text );
257 if ( $this->get_option( 'resolve_shortcodes' ) ) {
258 $text = apply_filters( 'the_content', $text );
259 }
260 else {
261 $pattern = "/\[[^\]]+\]/";
262 $text = preg_replace( $pattern, '', $text );
263 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
264 $text = preg_replace( $pattern, '', $text );
265 }
266 $text = $this->clean_text( $text );
267 $text = $this->clean_sentences( $text );
268 $text = apply_filters( 'mwai_post_content', $text, $postId );
269 return $text;
270 }
271
272 function markdown_to_html( $content ) {
273 $Parsedown = new Parsedown();
274 $content = $Parsedown->text( $content );
275 return $content;
276 }
277
278 function get_post_language( $postId ) {
279 $locale = get_locale();
280 $code = strtolower( substr( $locale, 0, 2 ) );
281 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
282 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
283 if ( !empty( $lang ) ) {
284 $locale = $lang['locale'];
285 $humanLanguage = $lang['display_name'];
286 }
287 return strtolower( "$locale ($humanLanguage)" );
288 }
289
290 function do_placeholders( $text ) {
291 $defaultPlaceholders = [];
292 $dataPlaceholders = $this->get_user_data();
293 if ( !empty( $dataPlaceholders ) ) {
294 $defaultPlaceholders = array_merge( $defaultPlaceholders, $dataPlaceholders );
295 }
296 $placeholders = apply_filters( 'mwai_placeholders', $defaultPlaceholders );
297 foreach ( $placeholders as $key => $value ) {
298 $text = str_replace( '{' . $key . '}', $value, $text );
299 }
300 return $text;
301 }
302 #endregion
303
304 #region Image-Related Helpers
305 static function is_image( $file ) {
306 $mimeType = Meow_MWAI_Core::get_mime_type( $file );
307 if ( strpos( $mimeType, 'image' ) !== false ) {
308 return true;
309 }
310 return false;
311 }
312
313 static function get_image_resolution( $url ) {
314 if ( empty( $url ) ) {
315 return null;
316 }
317 $headers = get_headers( $url, 1 );
318 if ( strpos( $headers[0], '200' ) === false ) {
319 return null;
320 }
321 $image_info = getimagesize( $url );
322 if ( $image_info === false ) {
323 return null;
324 }
325 return [
326 'width' => $image_info[0],
327 'height' => $image_info[1]
328 ];
329 }
330
331 static function get_mime_type( $file ) {
332 $mimeType = null;
333
334 // Let's try to use mime_content_type if the function exists
335 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
336 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
337 $mimeType = mime_content_type( $file );
338 }
339
340 // Otherwise, let's check the file extension (which can actually also be an URL)
341 if ( !$mimeType ) {
342 $extension = pathinfo( $file, PATHINFO_EXTENSION );
343 $extension = strtolower( $extension );
344 $mimeTypes = [
345 'jpg' => 'image/jpeg',
346 'jpeg' => 'image/jpeg',
347 'png' => 'image/png',
348 'gif' => 'image/gif',
349 'webp' => 'image/webp',
350 'bmp' => 'image/bmp',
351 'tiff' => 'image/tiff',
352 'tif' => 'image/tiff',
353 'svg' => 'image/svg+xml',
354 'ico' => 'image/x-icon',
355 'pdf' => 'application/pdf',
356 ];
357 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
358 }
359
360 return $mimeType;
361 }
362
363 function download_image( $url ) {
364 $args = array( 'timeout' => 60, );
365 $response = wp_safe_remote_get( $url, $args );
366 if ( is_wp_error( $response ) ) {
367 throw new Exception( $response->get_error_message() );
368 }
369 $output = wp_remote_retrieve_body( $response );
370 if ( is_wp_error( $output ) ) {
371 throw new Exception( $output->get_error_message() );
372 }
373 return $output;
374 }
375
376 /**
377 * Add an image from a URL to the Media Library.
378 * @param string $url The URL of the image to be downloaded.
379 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
380 * @param string $title The title of the image.
381 * @param string $description The description of the image.
382 * @param string $caption The caption of the image.
383 * @param string $alt The alt text of the image.
384 * @return int The attachment ID of the image.
385 */
386 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null ) {
387 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
388 $url_filename = $path_parts['basename'];
389 $file_type = wp_check_filetype( $url_filename, null );
390 $allowed_types = get_allowed_mime_types();
391 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
392 throw new Exception( 'Invalid file type from URL.' );
393 }
394
395 // Initial extension from URL file name
396 $extension = $file_type['ext'];
397
398 if ( !empty( $filename ) ) {
399 $custom_file_type = wp_check_filetype( $filename, null );
400 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
401 throw new Exception( 'Invalid custom file type.' );
402 }
403 // Use the extension from the custom filename if valid
404 $extension = $custom_file_type['ext'];
405 }
406
407 $image_data = $this->download_image( $url );
408 if ( !$image_data ) {
409 throw new Exception( 'Could not download the image.' );
410 }
411 $upload_dir = wp_upload_dir();
412
413 // Filename handling including 'generated_' prefix scenario
414 if ( empty( $filename ) ) {
415 $filename = sanitize_file_name( $url_filename );
416 if ( empty( $extension ) ) { // This condition might now be redundant
417 $extension = $file_type['ext'];
418 }
419 // Filename length check and prepend if conditions met
420 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
421 $filename = $this->get_random_id( 16 ) . '.' . $extension;
422 }
423 if ( strpos( $filename, '.' ) === false ) {
424 $filename .= '.' . $extension;
425 }
426 }
427
428 // Directory and file path handling
429 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
430 $file = $upload_dir['path'] . '/' . $filename;
431 }
432 else {
433 $file = $upload_dir['basedir'] . '/' . $filename;
434 }
435
436 // Ensure file name uniqueness in the directory
437 $i = 1;
438 $parts = pathinfo( $file );
439 while ( file_exists( $file ) ) {
440 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
441 $i++;
442 }
443
444 // Writing the file to disk
445 file_put_contents( $file, $image_data );
446
447 // Attachment and metadata handling in WP
448 $attachment = [
449 'post_mime_type' => $file_type['type'],
450 'post_title' => $title ?? '',
451 'post_content' => $description ?? '',
452 'post_excerpt' => $caption ?? '',
453 'post_status' => 'inherit'
454 ];
455 $attachmentId = wp_insert_attachment( $attachment, $file );
456 require_once( ABSPATH . 'wp-admin/includes/image.php' );
457 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
458 wp_update_attachment_metadata( $attachmentId, $attachment_data );
459 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
460
461 // Attach the image to a post if needed
462 if ( $attachedPost ) {
463 wp_update_post( [ 'ID' => $attachmentId, 'post_parent' => $attachedPost ] );
464 }
465
466 return $attachmentId;
467 }
468 #endregion
469
470 #region Context-Related Helpers
471 function retrieve_context( $params, $query ) {
472 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
473 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
474 $context = apply_filters( 'mwai_context_search', [], $query, [
475 'embeddingsEnvId' => $embeddingsEnvId
476 ]);
477 if ( empty( $context ) ) {
478 return null;
479 }
480 else if ( !isset( $context['content'] ) ) {
481 Meow_MWAI_Logging::warn( "A context without content was returned." );
482 return null;
483 }
484 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
485 $context['length'] = strlen( $context['content'] );
486 return $context;
487 }
488 #endregion
489
490 #region Users/Sessions Helpers
491
492 function get_nonce( $force = false ) {
493 if ( !$force && !is_user_logged_in() ) {
494 return null;
495 }
496 if ( isset( $this->nonce ) ) {
497 return $this->nonce;
498 }
499 $this->nonce = wp_create_nonce( 'wp_rest' );
500 return $this->nonce;
501 }
502
503 // This is a bit hacky, but chatId needs to be retrieved or generated.
504 // Maybe we can clean this up later.
505 function fix_chat_id( $query, $params ) {
506 if ( isset( $query->chatId ) && $query->chatId !== 'N/A' ) {
507 return $query->chatId;
508 }
509 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : $query->session;
510 if ( $chatId === 'N/A' ) {
511 $chatId = $this->get_random_id( 8 );
512 }
513 $query->set_chat_id( $chatId );
514 return $chatId;
515 }
516
517 function get_session_id() {
518 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
519 return $_COOKIE['mwai_session_id'];
520 }
521 return "N/A";
522 }
523
524 // Get the UserID from the data, or from the current user
525 function get_user_id( $data = null ) {
526 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
527 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
528 $admin = get_users( [ 'role' => 'administrator' ] );
529 if ( !empty( $admin ) ) {
530 return $admin[0]->ID;
531 }
532 }
533 if ( isset( $data ) && isset( $data['userId'] ) ) {
534 return (int)$data['userId'];
535 }
536 if ( is_user_logged_in() ) {
537 $current_user = wp_get_current_user();
538 if ( $current_user->ID > 0 ) {
539 return $current_user->ID;
540 }
541 }
542 return null;
543 }
544
545 function get_admin_user() {
546 $admin = get_users( [ 'role' => 'administrator' ] );
547 if ( !empty( $admin ) ) {
548 return $admin[0];
549 }
550 return null;
551 }
552
553 function get_user_data() {
554 $user = wp_get_current_user();
555 if ( empty( $user ) || empty( $user->ID ) ) {
556 return null;
557 }
558 $placeholders = array(
559 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
560 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
561 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
562 $user->data->user_login : null,
563 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
564 $user->data->display_name : null,
565 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
566 );
567 return $placeholders;
568 }
569
570 function get_ip_address( $params = null ) {
571 $ip = '127.0.0.1';
572 $headers = [
573 'HTTP_TRUE_CLIENT_IP',
574 'HTTP_CF_CONNECTING_IP',
575 'HTTP_X_REAL_IP',
576 'HTTP_CLIENT_IP',
577 'HTTP_X_FORWARDED_FOR',
578 'HTTP_X_FORWARDED',
579 'HTTP_X_CLUSTER_CLIENT_IP',
580 'HTTP_FORWARDED_FOR',
581 'HTTP_FORWARDED',
582 'REMOTE_ADDR',
583 ];
584
585 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
586 $ip = ( string )$params[ 'ip' ];
587 } else {
588 foreach ( $headers as $header ) {
589 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
590 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
591 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
592 break;
593 }
594 }
595 }
596
597 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
598 }
599
600 #endregion
601
602 #region Sanitization
603 function sanitize_sort( &$sort, $default_accessor = 'created', $default_order = 'DESC',
604 $allowed_columns = array( 'created', 'updated', 'name', 'id', 'time', 'units', 'price' )) {
605
606 // Ensure $sort is an array
607 if ( !is_array( $sort ) ) {
608 $sort = [ "accessor" => $default_accessor, "by" => $default_order ];
609 }
610 // Extract and sanitize the accessor
611 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
612 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
613 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
614 $sort_accessor = $default_accessor;
615 }
616 // Extract and sanitize the sort order
617 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
618 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
619 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
620 $sort_by = $default_order;
621 }
622 // Update the sort array with sanitized values
623 $sort['accessor'] = $sort_accessor;
624 $sort['by'] = $sort_by;
625 }
626 #endregion
627
628 #region Other Helpers
629 function safe_strlen( $string, $encoding = 'UTF-8' ) {
630 if ( function_exists( 'mb_strlen' ) ) {
631 return mb_strlen( $string, $encoding );
632 }
633 else {
634 // Fallback implementation for environments without mbstring extension
635 return preg_match_all( '/./u', $string, $matches );
636 }
637 }
638
639 public function check_rest_nonce( $request ) {
640 $nonce = $request->get_header( 'X-WP-Nonce' );
641 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
642 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
643 }
644
645 function get_random_id( $length = 8, $excludeIds = [] ) {
646 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
647 $charactersLength = strlen( $characters );
648 $randomId = '';
649 for ( $i = 0; $i < $length; $i++ ) {
650 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
651 }
652 if ( in_array( $randomId, $excludeIds ) ) {
653 return $this->get_random_id( $length, $excludeIds );
654 }
655 return $randomId;
656 }
657
658 function is_url( $url ) {
659 return strpos( $url, 'http' ) === 0 ? true : false;
660 }
661
662 function get_post_types() {
663 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
664 $post_types = array();
665 $types = get_post_types( [], 'objects' );
666
667 // Let's get the Post Types that are enabled for Embeddings Sync
668 $embeddingsSettings = $this->get_option( 'embeddings' );
669 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
670
671 foreach ( $types as $type ) {
672 $forced = in_array( $type->name, $syncPostTypes );
673 // Should not be excluded.
674 if ( !$forced && in_array( $type->name, $excluded ) ) {
675 continue;
676 }
677 // Should be public.
678 if ( !$forced && !$type->public ) {
679 continue;
680 }
681 $post_types[] = array(
682 'name' => $type->labels->name,
683 'type' => $type->name,
684 );
685 }
686
687 // Let's get the Post Types that are enabled for Embeddings Sync
688 $embeddingsSettings = $this->get_option( 'embeddings' );
689 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
690
691 return $post_types;
692 }
693
694 function get_post( $post ) {
695 if ( is_numeric( $post ) ) {
696 $post = get_post( $post );
697 }
698 if ( is_object( $post ) ) {
699 $post = (array)$post;
700 }
701 if ( !is_array( $post ) ) {
702 return null;
703 }
704 $language = $this->get_post_language( $post['ID'] );
705 $content = $this->get_post_content( $post['ID'] );
706 $title = $post['post_title'];
707 $excerpt = $post['post_excerpt'];
708 $url = get_permalink( $post['ID'] );
709 $checksum = wp_hash( $content . $title . $url );
710 return [
711 'postId' => (int)$post['ID'],
712 'title' => $title,
713 'content' => $content,
714 'excerpt' => $excerpt,
715 'url' => $url,
716 'language' => $language ?? 'english',
717 'checksum' => $checksum,
718 ];
719 }
720 #endregion
721
722 #region Usage & Costs
723
724 // Quick and dirty token estimation
725 // Let's keep this synchronized with Helpers in JS
726 static function estimate_tokens( ...$args ): int {
727 $text = "";
728 foreach ( $args as $arg ) {
729 if ( is_array( $arg ) ) {
730 foreach ( $arg as $message ) {
731 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
732 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
733 }
734 }
735 else if ( is_string( $arg ) ) {
736 $text .= $arg;
737 }
738 }
739 $averageTokenLength = 4;
740 $words = preg_split( '/\s+/', trim( $text ) );
741 $tokenCount = 0;
742 foreach ( $words as $word ) {
743 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
744 }
745 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
746 }
747
748 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
749 if ( !is_numeric( $in_tokens ) ) {
750 throw new Exception( 'AI Engine: in_tokens must be a number.' );
751 }
752 if ( !is_numeric( $out_tokens ) ) {
753 $out_tokens = 0;
754 }
755 if ( !$model ) {
756 throw new Exception( 'AI Engine: model is required.' );
757 }
758 $usage = $this->get_option( 'ai_models_usage' );
759 $month = date( 'Y-m' );
760 if ( !isset( $usage[$month] ) ) {
761 $usage[$month] = array();
762 }
763 if ( !isset( $usage[$month][$model] ) ) {
764 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
765 }
766 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
767 $usage[$month][$model]['completion_tokens'] += $out_tokens;
768 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
769 $this->update_option( 'ai_models_usage', $usage );
770 $usageInfo = [
771 'prompt_tokens' => $in_tokens,
772 'completion_tokens' => $out_tokens,
773 'total_tokens' => $in_tokens + $out_tokens,
774 ];
775 if ( $returned_price !== null ) {
776 $usageInfo['price'] = $returned_price;
777 }
778 return $usageInfo;
779 }
780
781 public function record_audio_usage( $model, $seconds ) {
782 if ( !is_numeric( $seconds ) ) {
783 throw new Exception( 'AI Engine: seconds must be a number.' );
784 }
785 if ( !$model ) {
786 throw new Exception( 'AI Engine: model is required.' );
787 }
788 $usage = $this->get_option( 'ai_models_usage' );
789 $month = date( 'Y-m' );
790 if ( !isset( $usage[$month] ) ) {
791 $usage[$month] = array();
792 }
793 if ( !isset( $usage[$month][$model] ) ) {
794 $usage[$month][$model] = array( 'seconds' => 0 );
795 }
796 $usage[$month][$model]['seconds'] += $seconds;
797 $this->update_option( 'ai_models_usage', $usage );
798 return [ 'seconds' => $seconds ];
799 }
800
801 public function record_images_usage( $model, $resolution, $images ) {
802 if ( !$model || !$resolution || !$images ) {
803 throw new Exception( 'Missing parameters for record_image_usage.' );
804 }
805 $usage = $this->get_option( 'ai_models_usage' );
806 $month = date( 'Y-m' );
807 if ( !isset( $usage[$month] ) ) {
808 $usage[$month] = array();
809 }
810 if ( !isset( $usage[$month][$model] ) ) {
811 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
812 }
813 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
814 $usage[$month][$model]['resolution'][$resolution] = 0;
815 }
816 $usage[$month][$model]['resolution'][$resolution] += $images;
817 $usage[$month][$model]['images'] += $images;
818 $this->update_option( 'ai_models_usage', $usage );
819 return [ 'resolution' => $resolution, 'images' => $images ];
820 }
821
822 #endregion
823
824 #region Streaming
825 public function stream_push( $data, $query = null ) {
826 $data = apply_filters( 'mwai_stream_push', $data, $query );
827 $out = "data: " . json_encode( $data );
828 echo $out;
829 echo "\n\n";
830 if ( ob_get_level() > 0 ) {
831 ob_end_flush();
832 }
833 flush();
834 }
835 #endregion
836
837 #region Options
838 function get_themes() {
839 $themes = get_option( $this->themes_option_name, [] );
840 $themes = empty( $themes ) ? [] : $themes;
841
842 $internalThemes = [
843 'chatgpt' => [
844 'type' => 'internal','name' => 'ChatGPT', 'themeId' => 'chatgpt',
845 'settings' => [], 'style' => ""
846 ],
847 'messages' => [
848 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
849 'settings' => [], 'style' => ""
850 ],
851 'timeless' => [
852 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
853 'settings' => [], 'style' => ""
854 ],
855 ];
856 $customThemes = [];
857 foreach ( $themes as $theme ) {
858 if ( isset( $internalThemes[$theme['themeId']] ) ) {
859 $internalThemes[$theme['themeId']] = $theme;
860 continue;
861 }
862 $customThemes[] = $theme;
863 }
864 return array_merge(array_values($internalThemes), $customThemes);
865 }
866
867 function update_themes( $themes ) {
868 update_option( $this->themes_option_name, $themes );
869 return $themes;
870 }
871
872 function get_chatbots() {
873 $chatbots = get_option( $this->chatbots_option_name, [] );
874 $hasChanges = false;
875 if ( empty( $chatbots ) ) {
876 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
877 }
878 $hasDefault = false;
879 foreach ( $chatbots as &$chatbot ) {
880 if ( $chatbot['botId'] === 'default' ) {
881 $hasDefault = true;
882 }
883 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
884 // Use default value if not set.
885 if ( !isset( $chatbot[$key] ) ) {
886 $chatbot[$key] = $value;
887 }
888 }
889 // TODO: After October 2024, let's remove this.
890 if ( isset( $chatbot['context'] ) ) {
891 $chatbot['instructions'] = $chatbot['context'];
892 unset( $chatbot['context'] );
893 $hasChanges = true;
894 }
895 // TODO: After October 2024, let's remove this.
896 if ( isset( $chatbot['fileUpload'] ) ) {
897 $chatbot['fileSearch'] = $chatbot['fileUpload'];
898 unset( $chatbot['fileUpload'] );
899 $hasChanges = true;
900 }
901 }
902 if ( !$hasDefault ) {
903 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
904 array_unshift( $chatbots, $defaultBot );
905 $hasChanges = true;
906 }
907 if ( $hasChanges ) {
908 update_option( $this->chatbots_option_name, $chatbots );
909 }
910 return $chatbots;
911 }
912
913 function get_chatbot( $botId ) {
914 $chatbots = $this->get_chatbots();
915 foreach ( $chatbots as $chatbot ) {
916 if ( $chatbot['botId'] === (string)$botId ) {
917 return $chatbot;
918 }
919 }
920 return null;
921 }
922
923 function get_embeddings_env( $envId ) {
924 $envs = $this->get_option( 'embeddings_envs' );
925 foreach ( $envs as $env ) {
926 if ( $env['id'] === $envId ) {
927 return $env;
928 }
929 }
930 return null;
931 }
932
933 function get_ai_env( $envId ) {
934 $envs = $this->get_option( 'ai_envs' );
935 foreach ( $envs as $env ) {
936 if ( $env['id'] === $envId ) {
937 return $env;
938 }
939 }
940 return null;
941 }
942
943 function get_assistant( $envId, $assistantId ) {
944 $env = $this->get_ai_env( $envId );
945 if ( !$env ) {
946 return null;
947 }
948 $assistants = $env['assistants'];
949 foreach ( $assistants as $assistant ) {
950 if ( $assistant['id'] === $assistantId ) {
951 return $assistant;
952 }
953 }
954 return null;
955 }
956
957 function get_theme( $themeId ) {
958 $themes = $this->get_themes();
959 foreach ( $themes as $theme ) {
960 if ( $theme['themeId'] === $themeId ) {
961 return $theme;
962 }
963 }
964 return null;
965 }
966
967 function update_chatbots( $chatbots ) {
968 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
969 // TODO: I think some HTML fields are missing, guestName, maybe others.
970 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
971 $keepLineReturnsFields = [ 'instructions' ];
972 $whiteSpacedFields = [ 'context' ];
973 foreach ( $chatbots as &$chatbot ) {
974 foreach ( $chatbot as $key => &$value ) {
975 if ( in_array( $key, $deprecatedFields ) ) {
976 unset( $chatbot[$key] );
977 continue;
978 }
979 if ( in_array( $key, $htmlFields ) ) {
980 $value = wp_kses_post( $value );
981 }
982 else if ( in_array( $key, $whiteSpacedFields ) ) {
983 $value = sanitize_textarea_field( $value );
984 }
985 else if ( $key === 'functions' ) {
986 $functions = [];
987 foreach ( $value as $function ) {
988 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
989 $functions[] = [
990 'id' => sanitize_text_field( $function['id'] ),
991 'type' => sanitize_text_field( $function['type'] ),
992 ];
993 }
994 }
995 $value = $functions;
996 }
997 else {
998 if ( in_array( $key, $keepLineReturnsFields ) ) {
999 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
1000 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
1001 }
1002 $value = sanitize_text_field( $value );
1003 if ( in_array( $key, $keepLineReturnsFields ) ) {
1004 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1005 }
1006 }
1007 }
1008 }
1009 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1010 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1011 $chatbots = get_option( $this->chatbots_option_name, [] );
1012 return $chatbots;
1013 }
1014 return $chatbots;
1015 }
1016
1017 function populate_dynamic_options( $options ) {
1018 // Languages
1019 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1020
1021 // Consolidate the Engines and their Models
1022 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1023 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1024 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1025 foreach ( $options['ai_engines'] as &$engine ) {
1026 if ( $engine['type'] === 'openai' ) {
1027 $engine['models'] = apply_filters( 'mwai_openai_models',
1028 Meow_MWAI_Engines_OpenAI::get_models_static()
1029 );
1030 }
1031 else if ( $engine['type'] === 'anthropic' ) {
1032 $engine['models'] = apply_filters( 'mwai_anthropic_models',
1033 Meow_MWAI_Engines_Anthropic::get_models_static()
1034 );
1035 }
1036 else {
1037 $engine['models'] = [];
1038 foreach ( $options['ai_models'] as $model ) {
1039 if ( $model['type'] === $engine['type'] ) {
1040 $engine['models'][] = $model;
1041 }
1042 }
1043 }
1044 }
1045
1046 // Functions via Snippet Vault (or custom code)
1047 $json = [];
1048 $functions = apply_filters( 'mwai_functions_list', [] );
1049 foreach ( $functions as $function ) {
1050 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1051 }
1052 $options['functions'] = $json;
1053
1054 // Addons
1055 $options['addons'] = apply_filters( 'mwai_addons', [
1056 [
1057 'slug' => "mwai-notifications",
1058 'name' => "Notifications",
1059 'description' => "Add-on for AI Engine that adds notifications.",
1060 'install_url' => "https://meowapps.com/products/mwai-notifications/",
1061 'settings_url' => null,
1062 'enabled' => false,
1063 ], [
1064 'slug' => "mwai-ollama",
1065 'name' => "Ollama",
1066 'description' => "Support for local LLMs via Ollama. Select the 'Ollama' type in your 'Environments for AI', then you can 'Refresh Models' and use them!",
1067 'install_url' => "https://meowapps.com/products/mwai-ollama/",
1068 'settings_url' => null,
1069 'enabled' => false
1070 ], [
1071 'slug' => "mwai-websearch",
1072 'name' => "Web Search",
1073 'description' => "This add-on for AI Engine enhances the AI models' responses by incorporating additional context. It currently supports Google Search.",
1074 'install_url' => "https://meowapps.com/products/mwai-websearch/",
1075 'settings_url' => null,
1076 'enabled' => false
1077 ], [
1078 'slug' => "mwai-better-links",
1079 'name' => "Better Links",
1080 'description' => "This add-on for AI Engine adds link validation and term linking features.",
1081 'install_url' => "https://meowapps.com/products/mwai-better-links/",
1082 'settings_url' => null,
1083 'enabled' => false
1084 ], [
1085 'slug' => "mwai-woo-basics",
1086 'name' => "Woo Basics",
1087 'description' => "Handles basic WooCommerce functionalities with chatbots.",
1088 'install_url' => "https://meowapps.com/products/mwai-woo-basics/",
1089 'settings_url' => null,
1090 'enabled' => false
1091 ], [
1092 'slug' => "mwai-quick-actions",
1093 'name' => "Quick Actions",
1094 'description' => "Add dynamic quick actions to your chatbot, triggered by events or at chat start.",
1095 'install_url' => "https://meowapps.com/products/mwai-quick-actions/",
1096 'settings_url' => null,
1097 'enabled' => false
1098 ]
1099 ] );
1100
1101 return $options;
1102 }
1103
1104 function get_all_options( $force = false, $sanitize = false ) {
1105 if ( $force || is_null( $this->options ) ) {
1106 $options = get_option( $this->option_name, [] );
1107 $init_mode = empty( $options );
1108 foreach ( MWAI_OPTIONS as $key => $value ) {
1109 if ( !isset( $options[$key] ) ) {
1110 $options[$key] = $value;
1111 }
1112 }
1113 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1114 $options['default_limits'] = MWAI_LIMITS;
1115 if ( $sanitize || $init_mode ) {
1116 $options = $this->sanitize_options( $options );
1117 }
1118 $this->options = $options;
1119 }
1120 $options = $this->populate_dynamic_options( $this->options );
1121 return $options;
1122 }
1123
1124 // Sanitize options when we update the plugin or perform some updates
1125 // if we change the structure of the options.
1126 function sanitize_options( $options ) {
1127 $needs_update = false;
1128
1129 // TODO: After October 2024, let's remove this.
1130 $old_options = [
1131 'openai_models',
1132 'anthropic_models',
1133 '${envType}_models',
1134 'shortcode_chat_params',
1135 'extra_models',
1136 'fallback_model',
1137 'mwai_advisor_data'
1138 ];
1139 foreach ( $old_options as $old_option ) {
1140 if ( isset( $options[$old_option] ) ) {
1141 unset( $options[$old_option] );
1142 $needs_update = true;
1143 }
1144 }
1145
1146 // Avoid the logs_path to be a PHP file.
1147 if ( isset( $options['logs_path'] ) ) {
1148 $logs_path = $options['logs_path'];
1149 if ( substr( $logs_path, -4 ) !== '.log' ) {
1150 $options['logs_path'] = '';
1151 $needs_update = true;
1152 }
1153 }
1154
1155 // TODO: After October 2024, let's remove this.
1156 #region Temporary Code
1157 if ( isset( $options['openrouter_models'] ) ) {
1158 foreach ( $options['openrouter_models'] as $model ) {
1159 $model['envId'] = null;
1160 $model['type'] = 'openrouter';
1161 $options['ai_models'][] = $model;
1162 }
1163 $needs_update = true;
1164 unset( $options['openrouter_models'] );
1165 }
1166 if ( isset( $options['google_models'] ) ) {
1167 foreach ( $options['google_models'] as $model ) {
1168 $model['envId'] = null;
1169 $model['type'] = 'google';
1170 $options['ai_models'][] = $model;
1171 }
1172 $needs_update = true;
1173 unset( $options['google_models'] );
1174 }
1175 if ( isset( $options['shortcode_chat_stream'] ) ) {
1176 $options['ai_streaming'] = $options['shortcode_chat_stream'];
1177 unset( $options['shortcode_chat_stream'] );
1178 $needs_update = true;
1179 }
1180 if ( isset( $options['shortcode_chat_syntax_highlighting'] ) ) {
1181 $options['syntax_highlight'] = $options['shortcode_chat_syntax_highlighting'];
1182 unset( $options['shortcode_chat_syntax_highlighting'] );
1183 $needs_update = true;
1184 }
1185 if ( isset( $options['shortcode_chat_moderation'] ) ) {
1186 $options['chatbot_moderation'] = $options['shortcode_chat_moderation'];
1187 unset( $options['shortcode_chat_moderation'] );
1188 $needs_update = true;
1189 }
1190 if ( isset( $options['shortcode_chat_discussions'] ) ) {
1191 $options['chatbot_discussions'] = $options['shortcode_chat_discussions'];
1192 unset( $options['shortcode_chat_discussions'] );
1193 $needs_update = true;
1194 }
1195 if ( isset( $options['shortcode_chat_typewriter'] ) ) {
1196 $options['chatbot_typewriter'] = $options['shortcode_chat_typewriter'];
1197 unset( $options['shortcode_chat_typewriter'] );
1198 $needs_update = true;
1199 }
1200 if ( isset( $options['shortcode_chat'] ) ) {
1201 $options['module_chatbots'] = $options['shortcode_chat'];
1202 unset( $options['shortcode_chat'] );
1203 $needs_update = true;
1204 }
1205 if ( isset( $options['openai_usage'] ) ) {
1206 $options['ai_models_usage'] = $options['openai_usage'];
1207 unset( $options['openai_usage'] );
1208 $needs_update = true;
1209 }
1210 #endregion
1211
1212 // The IDs for the embeddings environments are generated here.
1213 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1214 $embeddings_default_exists = false;
1215 if ( isset( $options['embeddings_envs'] ) ) {
1216 foreach ( $options['embeddings_envs'] as &$env ) {
1217 if ( !isset( $env['id'] ) ) {
1218 $env['id'] = $this->get_random_id();
1219 $needs_update = true;
1220 }
1221 if ( $env['id'] === $options['embeddings_default_env'] ) {
1222 $embeddings_default_exists = true;
1223 }
1224 }
1225 }
1226 if ( !$embeddings_default_exists ) {
1227 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1228 $needs_update = true;
1229 }
1230
1231 // The IDs for the AI environments are generated here.
1232 $allEnvIds = [];
1233 $ai_default_exists = false;
1234 if ( isset( $options['ai_envs'] ) ) {
1235 foreach ( $options['ai_envs'] as &$env ) {
1236 if ( !isset( $env['id'] ) ) {
1237 $env['id'] = $this->get_random_id();
1238 $needs_update = true;
1239 }
1240 if ( $env['id'] === $options['ai_default_env'] ) {
1241 $ai_default_exists = true;
1242 }
1243 $allEnvIds[] = $env['id'];
1244 }
1245 }
1246 if ( !$ai_default_exists ) {
1247 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1248 $needs_update = true;
1249 }
1250
1251 // All the models with an envId that does not exist anymore are removed.
1252 if ( isset( $options['ai_models'] ) ) {
1253 $options['ai_models'] = array_values( array_filter( $options['ai_models'],
1254 function( $model ) use ( $allEnvIds, &$needs_update ) {
1255 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1256 $needs_update = true;
1257 return false;
1258 }
1259 return true;
1260 }
1261 ) );
1262 }
1263
1264 if ( $needs_update ) {
1265 ksort( $options );
1266 update_option( $this->option_name, $options, false );
1267 }
1268
1269 return $options;
1270 }
1271
1272 function update_options( $options ) {
1273 if ( !update_option( $this->option_name, $options, false ) ) {
1274 return false;
1275 }
1276 $options = $this->get_all_options( true, true );
1277 return $options;
1278 }
1279
1280 function update_option( $option, $value ) {
1281 $options = $this->get_all_options( true );
1282 $options[$option] = $value;
1283 return $this->update_options( $options );
1284 }
1285
1286 function get_option( $option, $default = null ) {
1287 $options = $this->get_all_options();
1288 return $options[$option] ?? $default;
1289 }
1290
1291 function update_ai_env( $env_id, $option, $value ) {
1292 $options = $this->get_all_options( true );
1293 foreach ( $options['ai_envs'] as &$env ) {
1294 if ( $env['id'] === $env_id ) {
1295 $env[$option] = $value;
1296 break;
1297 }
1298 }
1299 return $this->update_options( $options );
1300 }
1301
1302 function get_engine_models( $engineType ) {
1303 $engines = $this->get_option( 'ai_engines' );
1304 foreach ( $engines as $engine ) {
1305 if ( $engine['type'] === $engineType ) {
1306 return isset( $engine['models'] ) ? $engine['models'] : [];
1307 }
1308 }
1309 return [];
1310 }
1311
1312 function reset_options() {
1313 delete_option( $this->themes_option_name );
1314 delete_option( $this->chatbots_option_name );
1315 delete_option( $this->option_name );
1316 return $this->get_all_options( true );
1317 }
1318 #endregion
1319 }
1320
1321 ?>