PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.6
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / core.php
ai-engine / classes Last commit date
engines 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
1303 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 $user_data = $this->get_user_data();
292 $placeholders = apply_filters( 'mwai_placeholders', $user_data );
293 foreach ( $placeholders as $key => $value ) {
294 $text = str_replace( '{' . $key . '}', $value, $text );
295 }
296 return $text;
297 }
298 #endregion
299
300 #region Image-Related Helpers
301 static function is_image( $file ) {
302 $mimeType = Meow_MWAI_Core::get_mime_type( $file );
303 if ( strpos( $mimeType, 'image' ) !== false ) {
304 return true;
305 }
306 return false;
307 }
308
309 static function get_image_resolution( $url ) {
310 if ( empty( $url ) ) {
311 return null;
312 }
313 $headers = get_headers( $url, 1 );
314 if ( strpos( $headers[0], '200' ) === false ) {
315 return null;
316 }
317 $image_info = getimagesize( $url );
318 if ( $image_info === false ) {
319 return null;
320 }
321 return [
322 'width' => $image_info[0],
323 'height' => $image_info[1]
324 ];
325 }
326
327 static function get_mime_type( $file ) {
328 $mimeType = null;
329
330 // Let's try to use mime_content_type if the function exists
331 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
332 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
333 $mimeType = mime_content_type( $file );
334 }
335
336 // Otherwise, let's check the file extension (which can actually also be an URL)
337 if ( !$mimeType ) {
338 $extension = pathinfo( $file, PATHINFO_EXTENSION );
339 $extension = strtolower( $extension );
340 $mimeTypes = [
341 'jpg' => 'image/jpeg',
342 'jpeg' => 'image/jpeg',
343 'png' => 'image/png',
344 'gif' => 'image/gif',
345 'webp' => 'image/webp',
346 'bmp' => 'image/bmp',
347 'tiff' => 'image/tiff',
348 'tif' => 'image/tiff',
349 'svg' => 'image/svg+xml',
350 'ico' => 'image/x-icon',
351 'pdf' => 'application/pdf',
352 ];
353 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
354 }
355
356 return $mimeType;
357 }
358
359 function download_image( $url ) {
360 $args = array( 'timeout' => 60, );
361 $response = wp_safe_remote_get( $url, $args );
362 if ( is_wp_error( $response ) ) {
363 throw new Exception( $response->get_error_message() );
364 }
365 $output = wp_remote_retrieve_body( $response );
366 if ( is_wp_error( $output ) ) {
367 throw new Exception( $output->get_error_message() );
368 }
369 return $output;
370 }
371
372 /**
373 * Add an image from a URL to the Media Library.
374 * @param string $url The URL of the image to be downloaded.
375 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
376 * @param string $title The title of the image.
377 * @param string $description The description of the image.
378 * @param string $caption The caption of the image.
379 * @param string $alt The alt text of the image.
380 * @return int The attachment ID of the image.
381 */
382 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null ) {
383 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
384 $url_filename = $path_parts['basename'];
385 $file_type = wp_check_filetype( $url_filename, null );
386 $allowed_types = get_allowed_mime_types();
387 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
388 throw new Exception( 'Invalid file type from URL.' );
389 }
390
391 // Initial extension from URL file name
392 $extension = $file_type['ext'];
393
394 if ( !empty( $filename ) ) {
395 $custom_file_type = wp_check_filetype( $filename, null );
396 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
397 throw new Exception( 'Invalid custom file type.' );
398 }
399 // Use the extension from the custom filename if valid
400 $extension = $custom_file_type['ext'];
401 }
402
403 $image_data = $this->download_image( $url );
404 if ( !$image_data ) {
405 throw new Exception( 'Could not download the image.' );
406 }
407 $upload_dir = wp_upload_dir();
408
409 // Filename handling including 'generated_' prefix scenario
410 if ( empty( $filename ) ) {
411 $filename = sanitize_file_name( $url_filename );
412 if ( empty( $extension ) ) { // This condition might now be redundant
413 $extension = $file_type['ext'];
414 }
415 // Filename length check and prepend if conditions met
416 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
417 $filename = $this->get_random_id( 16 ) . '.' . $extension;
418 }
419 if ( strpos( $filename, '.' ) === false ) {
420 $filename .= '.' . $extension;
421 }
422 }
423
424 // Directory and file path handling
425 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
426 $file = $upload_dir['path'] . '/' . $filename;
427 }
428 else {
429 $file = $upload_dir['basedir'] . '/' . $filename;
430 }
431
432 // Ensure file name uniqueness in the directory
433 $i = 1;
434 $parts = pathinfo( $file );
435 while ( file_exists( $file ) ) {
436 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
437 $i++;
438 }
439
440 // Writing the file to disk
441 file_put_contents( $file, $image_data );
442
443 // Attachment and metadata handling in WP
444 $attachment = [
445 'post_mime_type' => $file_type['type'],
446 'post_title' => $title ?? '',
447 'post_content' => $description ?? '',
448 'post_excerpt' => $caption ?? '',
449 'post_status' => 'inherit'
450 ];
451 $attachmentId = wp_insert_attachment( $attachment, $file );
452 require_once( ABSPATH . 'wp-admin/includes/image.php' );
453 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
454 wp_update_attachment_metadata( $attachmentId, $attachment_data );
455 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
456
457 // Attach the image to a post if needed
458 if ( $attachedPost ) {
459 wp_update_post( [ 'ID' => $attachmentId, 'post_parent' => $attachedPost ] );
460 }
461
462 return $attachmentId;
463 }
464 #endregion
465
466 #region Context-Related Helpers
467 function retrieve_context( $params, $query ) {
468 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
469 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
470 $context = apply_filters( 'mwai_context_search', [], $query, [
471 'embeddingsEnvId' => $embeddingsEnvId
472 ]);
473 if ( empty( $context ) ) {
474 return null;
475 }
476 else if ( !isset( $context['content'] ) ) {
477 Meow_MWAI_Logging::warn( "A context without content was returned." );
478 return null;
479 }
480 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
481 $context['length'] = strlen( $context['content'] );
482 return $context;
483 }
484 #endregion
485
486 #region Users/Sessions Helpers
487
488 function get_nonce( $force = false ) {
489 if ( !$force && !is_user_logged_in() ) {
490 return null;
491 }
492 if ( isset( $this->nonce ) ) {
493 return $this->nonce;
494 }
495 $this->nonce = wp_create_nonce( 'wp_rest' );
496 return $this->nonce;
497 }
498
499 // This is a bit hacky, but chatId needs to be retrieved or generated.
500 // Maybe we can clean this up later.
501 function fix_chat_id( $query, $params ) {
502 if ( isset( $query->chatId ) && $query->chatId !== 'N/A' ) {
503 return $query->chatId;
504 }
505 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : $query->session;
506 if ( $chatId === 'N/A' ) {
507 $chatId = $this->get_random_id( 8 );
508 }
509 $query->set_chat_id( $chatId );
510 return $chatId;
511 }
512
513 function get_session_id() {
514 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
515 return $_COOKIE['mwai_session_id'];
516 }
517 return "N/A";
518 }
519
520 // Get the UserID from the data, or from the current user
521 function get_user_id( $data = null ) {
522 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
523 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
524 $admin = get_users( [ 'role' => 'administrator' ] );
525 if ( !empty( $admin ) ) {
526 return $admin[0]->ID;
527 }
528 }
529 if ( isset( $data ) && isset( $data['userId'] ) ) {
530 return (int)$data['userId'];
531 }
532 if ( is_user_logged_in() ) {
533 $current_user = wp_get_current_user();
534 if ( $current_user->ID > 0 ) {
535 return $current_user->ID;
536 }
537 }
538 return null;
539 }
540
541 function get_admin_user() {
542 $admin = get_users( [ 'role' => 'administrator' ] );
543 if ( !empty( $admin ) ) {
544 return $admin[0];
545 }
546 return null;
547 }
548
549 function get_user_data() {
550 $user = wp_get_current_user();
551 if ( empty( $user ) || empty( $user->ID ) ) {
552 return null;
553 }
554 $placeholders = array(
555 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
556 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
557 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
558 $user->data->user_login : null,
559 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
560 $user->data->display_name : null,
561 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
562 );
563 return $placeholders;
564 }
565
566 function get_ip_address( $params = null ) {
567 $ip = '127.0.0.1';
568 $headers = [
569 'HTTP_TRUE_CLIENT_IP',
570 'HTTP_CF_CONNECTING_IP',
571 'HTTP_X_REAL_IP',
572 'HTTP_CLIENT_IP',
573 'HTTP_X_FORWARDED_FOR',
574 'HTTP_X_FORWARDED',
575 'HTTP_X_CLUSTER_CLIENT_IP',
576 'HTTP_FORWARDED_FOR',
577 'HTTP_FORWARDED',
578 'REMOTE_ADDR',
579 ];
580
581 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
582 $ip = ( string )$params[ 'ip' ];
583 } else {
584 foreach ( $headers as $header ) {
585 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
586 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
587 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
588 break;
589 }
590 }
591 }
592
593 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
594 }
595
596 #endregion
597
598 #region Sanitization
599 function sanitize_sort( &$sort, $default_accessor = 'created', $default_order = 'DESC',
600 $allowed_columns = array( 'created', 'updated', 'name', 'id', 'time', 'units', 'price' )) {
601
602 // Ensure $sort is an array
603 if ( !is_array( $sort ) ) {
604 $sort = [ "accessor" => $default_accessor, "by" => $default_order ];
605 }
606 // Extract and sanitize the accessor
607 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
608 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
609 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
610 $sort_accessor = $default_accessor;
611 }
612 // Extract and sanitize the sort order
613 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
614 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
615 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
616 $sort_by = $default_order;
617 }
618 // Update the sort array with sanitized values
619 $sort['accessor'] = $sort_accessor;
620 $sort['by'] = $sort_by;
621 }
622 #endregion
623
624 #region Other Helpers
625 function safe_strlen( $string, $encoding = 'UTF-8' ) {
626 if ( function_exists( 'mb_strlen' ) ) {
627 return mb_strlen( $string, $encoding );
628 }
629 else {
630 // Fallback implementation for environments without mbstring extension
631 return preg_match_all( '/./u', $string, $matches );
632 }
633 }
634
635 public function check_rest_nonce( $request ) {
636 $nonce = $request->get_header( 'X-WP-Nonce' );
637 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
638 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
639 }
640
641 function get_random_id( $length = 8, $excludeIds = [] ) {
642 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
643 $charactersLength = strlen( $characters );
644 $randomId = '';
645 for ( $i = 0; $i < $length; $i++ ) {
646 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
647 }
648 if ( in_array( $randomId, $excludeIds ) ) {
649 return $this->get_random_id( $length, $excludeIds );
650 }
651 return $randomId;
652 }
653
654 function is_url( $url ) {
655 return strpos( $url, 'http' ) === 0 ? true : false;
656 }
657
658 function get_post_types() {
659 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
660 $post_types = array();
661 $types = get_post_types( [], 'objects' );
662
663 // Let's get the Post Types that are enabled for Embeddings Sync
664 $embeddingsSettings = $this->get_option( 'embeddings' );
665 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
666
667 foreach ( $types as $type ) {
668 $forced = in_array( $type->name, $syncPostTypes );
669 // Should not be excluded.
670 if ( !$forced && in_array( $type->name, $excluded ) ) {
671 continue;
672 }
673 // Should be public.
674 if ( !$forced && !$type->public ) {
675 continue;
676 }
677 $post_types[] = array(
678 'name' => $type->labels->name,
679 'type' => $type->name,
680 );
681 }
682
683 // Let's get the Post Types that are enabled for Embeddings Sync
684 $embeddingsSettings = $this->get_option( 'embeddings' );
685 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
686
687 return $post_types;
688 }
689
690 function get_post( $post ) {
691 if ( is_numeric( $post ) ) {
692 $post = get_post( $post );
693 }
694 if ( is_object( $post ) ) {
695 $post = (array)$post;
696 }
697 if ( !is_array( $post ) ) {
698 return null;
699 }
700 $language = $this->get_post_language( $post['ID'] );
701 $content = $this->get_post_content( $post['ID'] );
702 $title = $post['post_title'];
703 $excerpt = $post['post_excerpt'];
704 $url = get_permalink( $post['ID'] );
705 $checksum = wp_hash( $content . $title . $url );
706 return [
707 'postId' => (int)$post['ID'],
708 'title' => $title,
709 'content' => $content,
710 'excerpt' => $excerpt,
711 'url' => $url,
712 'language' => $language ?? 'english',
713 'checksum' => $checksum,
714 ];
715 }
716 #endregion
717
718 #region Usage & Costs
719
720 // Quick and dirty token estimation
721 // Let's keep this synchronized with Helpers in JS
722 static function estimate_tokens( ...$args ): int {
723 $text = "";
724 foreach ( $args as $arg ) {
725 if ( is_array( $arg ) ) {
726 foreach ( $arg as $message ) {
727 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
728 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
729 }
730 }
731 else if ( is_string( $arg ) ) {
732 $text .= $arg;
733 }
734 }
735 $averageTokenLength = 4;
736 $words = preg_split( '/\s+/', trim( $text ) );
737 $tokenCount = 0;
738 foreach ( $words as $word ) {
739 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
740 }
741 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
742 }
743
744 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
745 if ( !is_numeric( $in_tokens ) ) {
746 throw new Exception( 'AI Engine: in_tokens must be a number.' );
747 }
748 if ( !is_numeric( $out_tokens ) ) {
749 $out_tokens = 0;
750 }
751 if ( !$model ) {
752 throw new Exception( 'AI Engine: model is required.' );
753 }
754 $usage = $this->get_option( 'ai_models_usage' );
755 $month = date( 'Y-m' );
756 if ( !isset( $usage[$month] ) ) {
757 $usage[$month] = array();
758 }
759 if ( !isset( $usage[$month][$model] ) ) {
760 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
761 }
762 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
763 $usage[$month][$model]['completion_tokens'] += $out_tokens;
764 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
765 $this->update_option( 'ai_models_usage', $usage );
766 $usageInfo = [
767 'prompt_tokens' => $in_tokens,
768 'completion_tokens' => $out_tokens,
769 'total_tokens' => $in_tokens + $out_tokens,
770 ];
771 if ( $returned_price !== null ) {
772 $usageInfo['price'] = $returned_price;
773 }
774 return $usageInfo;
775 }
776
777 public function record_audio_usage( $model, $seconds ) {
778 if ( !is_numeric( $seconds ) ) {
779 throw new Exception( 'AI Engine: seconds must be a number.' );
780 }
781 if ( !$model ) {
782 throw new Exception( 'AI Engine: model is required.' );
783 }
784 $usage = $this->get_option( 'ai_models_usage' );
785 $month = date( 'Y-m' );
786 if ( !isset( $usage[$month] ) ) {
787 $usage[$month] = array();
788 }
789 if ( !isset( $usage[$month][$model] ) ) {
790 $usage[$month][$model] = array( 'seconds' => 0 );
791 }
792 $usage[$month][$model]['seconds'] += $seconds;
793 $this->update_option( 'ai_models_usage', $usage );
794 return [ 'seconds' => $seconds ];
795 }
796
797 public function record_images_usage( $model, $resolution, $images ) {
798 if ( !$model || !$resolution || !$images ) {
799 throw new Exception( 'Missing parameters for record_image_usage.' );
800 }
801 $usage = $this->get_option( 'ai_models_usage' );
802 $month = date( 'Y-m' );
803 if ( !isset( $usage[$month] ) ) {
804 $usage[$month] = array();
805 }
806 if ( !isset( $usage[$month][$model] ) ) {
807 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
808 }
809 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
810 $usage[$month][$model]['resolution'][$resolution] = 0;
811 }
812 $usage[$month][$model]['resolution'][$resolution] += $images;
813 $usage[$month][$model]['images'] += $images;
814 $this->update_option( 'ai_models_usage', $usage );
815 return [ 'resolution' => $resolution, 'images' => $images ];
816 }
817
818 #endregion
819
820 #region Streaming
821 public function stream_push( $data ) {
822 $data = apply_filters( 'mwai_stream_push', $data );
823 $out = "data: " . json_encode( $data );
824 echo $out;
825 echo "\n\n";
826 if ( ob_get_level() > 0 ) {
827 ob_end_flush();
828 }
829 flush();
830 }
831 #endregion
832
833 #region Options
834 function get_themes() {
835 $themes = get_option( $this->themes_option_name, [] );
836 $themes = empty( $themes ) ? [] : $themes;
837
838 $internalThemes = [
839 'chatgpt' => [
840 'type' => 'internal','name' => 'ChatGPT', 'themeId' => 'chatgpt',
841 'settings' => [], 'style' => ""
842 ],
843 'messages' => [
844 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
845 'settings' => [], 'style' => ""
846 ],
847 'timeless' => [
848 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
849 'settings' => [], 'style' => ""
850 ],
851 ];
852 $customThemes = [];
853 foreach ( $themes as $theme ) {
854 if ( isset( $internalThemes[$theme['themeId']] ) ) {
855 $internalThemes[$theme['themeId']] = $theme;
856 continue;
857 }
858 $customThemes[] = $theme;
859 }
860 return array_merge(array_values($internalThemes), $customThemes);
861 }
862
863 function update_themes( $themes ) {
864 update_option( $this->themes_option_name, $themes );
865 return $themes;
866 }
867
868 function get_chatbots() {
869 $chatbots = get_option( $this->chatbots_option_name, [] );
870 $hasChanges = false;
871 if ( empty( $chatbots ) ) {
872 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
873 }
874 $hasDefault = false;
875 foreach ( $chatbots as &$chatbot ) {
876 if ( $chatbot['botId'] === 'default' ) {
877 $hasDefault = true;
878 }
879 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
880 // Use default value if not set.
881 if ( !isset( $chatbot[$key] ) ) {
882 $chatbot[$key] = $value;
883 }
884 }
885 // TODO: After October 2024, let's remove this.
886 if ( isset( $chatbot['context'] ) ) {
887 $chatbot['instructions'] = $chatbot['context'];
888 unset( $chatbot['context'] );
889 $hasChanges = true;
890 }
891 // TODO: After October 2024, let's remove this.
892 if ( isset( $chatbot['fileUpload'] ) ) {
893 $chatbot['fileSearch'] = $chatbot['fileUpload'];
894 unset( $chatbot['fileUpload'] );
895 $hasChanges = true;
896 }
897 }
898 if ( !$hasDefault ) {
899 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
900 array_unshift( $chatbots, $defaultBot );
901 $hasChanges = true;
902 }
903 if ( $hasChanges ) {
904 update_option( $this->chatbots_option_name, $chatbots );
905 }
906 return $chatbots;
907 }
908
909 function get_chatbot( $botId ) {
910 $chatbots = $this->get_chatbots();
911 foreach ( $chatbots as $chatbot ) {
912 if ( $chatbot['botId'] === (string)$botId ) {
913 return $chatbot;
914 }
915 }
916 return null;
917 }
918
919 function get_embeddings_env( $envId ) {
920 $envs = $this->get_option( 'embeddings_envs' );
921 foreach ( $envs as $env ) {
922 if ( $env['id'] === $envId ) {
923 return $env;
924 }
925 }
926 return null;
927 }
928
929 function get_ai_env( $envId ) {
930 $envs = $this->get_option( 'ai_envs' );
931 foreach ( $envs as $env ) {
932 if ( $env['id'] === $envId ) {
933 return $env;
934 }
935 }
936 return null;
937 }
938
939 function get_assistant( $envId, $assistantId ) {
940 $env = $this->get_ai_env( $envId );
941 if ( !$env ) {
942 return null;
943 }
944 $assistants = $env['assistants'];
945 foreach ( $assistants as $assistant ) {
946 if ( $assistant['id'] === $assistantId ) {
947 return $assistant;
948 }
949 }
950 return null;
951 }
952
953 function get_theme( $themeId ) {
954 $themes = $this->get_themes();
955 foreach ( $themes as $theme ) {
956 if ( $theme['themeId'] === $themeId ) {
957 return $theme;
958 }
959 }
960 return null;
961 }
962
963 function update_chatbots( $chatbots ) {
964 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
965 // TODO: I think some HTML fields are missing, guestName, maybe others.
966 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
967 $keepLineReturnsFields = [ 'instructions' ];
968 $whiteSpacedFields = [ 'context' ];
969 foreach ( $chatbots as &$chatbot ) {
970 foreach ( $chatbot as $key => &$value ) {
971 if ( in_array( $key, $deprecatedFields ) ) {
972 unset( $chatbot[$key] );
973 continue;
974 }
975 if ( in_array( $key, $htmlFields ) ) {
976 $value = wp_kses_post( $value );
977 }
978 else if ( in_array( $key, $whiteSpacedFields ) ) {
979 $value = sanitize_textarea_field( $value );
980 }
981 else if ( $key === 'functions' ) {
982 $functions = [];
983 foreach ( $value as $function ) {
984 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
985 $functions[] = [
986 'id' => sanitize_text_field( $function['id'] ),
987 'type' => sanitize_text_field( $function['type'] ),
988 ];
989 }
990 }
991 $value = $functions;
992 }
993 else {
994 if ( in_array( $key, $keepLineReturnsFields ) ) {
995 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
996 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
997 }
998 $value = sanitize_text_field( $value );
999 if ( in_array( $key, $keepLineReturnsFields ) ) {
1000 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1001 }
1002 }
1003 }
1004 }
1005 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1006 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1007 $chatbots = get_option( $this->chatbots_option_name, [] );
1008 return $chatbots;
1009 }
1010 return $chatbots;
1011 }
1012
1013 function populate_dynamic_options( $options ) {
1014 // Languages
1015 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1016
1017 // Consolidate the Engines and their Models
1018 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1019 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1020 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1021 foreach ( $options['ai_engines'] as &$engine ) {
1022 if ( $engine['type'] === 'openai' ) {
1023 $engine['models'] = apply_filters( 'mwai_openai_models',
1024 Meow_MWAI_Engines_OpenAI::get_models_static()
1025 );
1026 }
1027 else if ( $engine['type'] === 'anthropic' ) {
1028 $engine['models'] = apply_filters( 'mwai_anthropic_models',
1029 Meow_MWAI_Engines_Anthropic::get_models_static()
1030 );
1031 }
1032 else {
1033 $engine['models'] = [];
1034 foreach ( $options['ai_models'] as $model ) {
1035 if ( $model['type'] === $engine['type'] ) {
1036 $engine['models'][] = $model;
1037 }
1038 }
1039 }
1040 }
1041
1042 // Functions via Snippet Vault (or custom code)
1043 $json = [];
1044 $functions = apply_filters( 'mwai_functions_list', [] );
1045 foreach ( $functions as $function ) {
1046 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1047 }
1048 $options['functions'] = $json;
1049
1050 // Addons
1051 $options['addons'] = apply_filters( 'mwai_addons', [
1052 [
1053 'slug' => "mwai-notifications",
1054 'name' => "Notifications",
1055 'description' => "Add-on for AI Engine that adds notifications.",
1056 'install_url' => "https://meowapps.com/products/mwai-notifications/",
1057 'settings_url' => null,
1058 'enabled' => false,
1059 ], [
1060 'slug' => "mwai-ollama",
1061 'name' => "Ollama",
1062 'description' => "Support for local LLMs via Ollama. Select the 'Ollama' type in your 'Environments for AI', then you can 'Refresh Models' and use them!",
1063 'install_url' => "https://meowapps.com/products/mwai-ollama/",
1064 'settings_url' => null,
1065 'enabled' => false
1066 ], [
1067 'slug' => "mwai-websearch",
1068 'name' => "Web Search",
1069 'description' => "This add-on for AI Engine enhances the AI models' responses by incorporating additional context. It currently supports Google Search.",
1070 'install_url' => "https://meowapps.com/products/mwai-websearch/",
1071 'settings_url' => null,
1072 'enabled' => false
1073 ], [
1074 'slug' => "mwai-better-links",
1075 'name' => "Better Links",
1076 'description' => "This add-on for AI Engine adds link validation and term linking features.",
1077 'install_url' => "https://meowapps.com/products/mwai-better-links/",
1078 'settings_url' => null,
1079 'enabled' => false
1080 ]
1081 ] );
1082
1083 return $options;
1084 }
1085
1086 function get_all_options( $force = false, $sanitize = false ) {
1087 if ( $force || is_null( $this->options ) ) {
1088 $options = get_option( $this->option_name, [] );
1089 $init_mode = empty( $options );
1090 foreach ( MWAI_OPTIONS as $key => $value ) {
1091 if ( !isset( $options[$key] ) ) {
1092 $options[$key] = $value;
1093 }
1094 }
1095 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1096 $options['default_limits'] = MWAI_LIMITS;
1097 if ( $sanitize || $init_mode ) {
1098 $options = $this->sanitize_options( $options );
1099 }
1100 $this->options = $options;
1101 }
1102 $options = $this->populate_dynamic_options( $this->options );
1103 return $options;
1104 }
1105
1106 // Sanitize options when we update the plugin or perform some updates
1107 // if we change the structure of the options.
1108 function sanitize_options( $options ) {
1109 $needs_update = false;
1110
1111 // TODO: After October 2024, let's remove this.
1112 $old_options = [
1113 'openai_models',
1114 'anthropic_models',
1115 '${envType}_models',
1116 'shortcode_chat_params',
1117 'extra_models',
1118 'fallback_model',
1119 'mwai_advisor_data'
1120 ];
1121 foreach ( $old_options as $old_option ) {
1122 if ( isset( $options[$old_option] ) ) {
1123 unset( $options[$old_option] );
1124 $needs_update = true;
1125 }
1126 }
1127
1128 // Avoid the logs_path to be a PHP file.
1129 if ( isset( $options['logs_path'] ) ) {
1130 $logs_path = $options['logs_path'];
1131 if ( substr( $logs_path, -4 ) !== '.log' ) {
1132 $options['logs_path'] = '';
1133 $needs_update = true;
1134 }
1135 }
1136
1137 // TODO: After October 2024, let's remove this.
1138 #region Temporary Code
1139 if ( isset( $options['openrouter_models'] ) ) {
1140 foreach ( $options['openrouter_models'] as $model ) {
1141 $model['envId'] = null;
1142 $model['type'] = 'openrouter';
1143 $options['ai_models'][] = $model;
1144 }
1145 $needs_update = true;
1146 unset( $options['openrouter_models'] );
1147 }
1148 if ( isset( $options['google_models'] ) ) {
1149 foreach ( $options['google_models'] as $model ) {
1150 $model['envId'] = null;
1151 $model['type'] = 'google';
1152 $options['ai_models'][] = $model;
1153 }
1154 $needs_update = true;
1155 unset( $options['google_models'] );
1156 }
1157 if ( isset( $options['shortcode_chat_stream'] ) ) {
1158 $options['ai_streaming'] = $options['shortcode_chat_stream'];
1159 unset( $options['shortcode_chat_stream'] );
1160 $needs_update = true;
1161 }
1162 if ( isset( $options['shortcode_chat_syntax_highlighting'] ) ) {
1163 $options['syntax_highlight'] = $options['shortcode_chat_syntax_highlighting'];
1164 unset( $options['shortcode_chat_syntax_highlighting'] );
1165 $needs_update = true;
1166 }
1167 if ( isset( $options['shortcode_chat_moderation'] ) ) {
1168 $options['chatbot_moderation'] = $options['shortcode_chat_moderation'];
1169 unset( $options['shortcode_chat_moderation'] );
1170 $needs_update = true;
1171 }
1172 if ( isset( $options['shortcode_chat_discussions'] ) ) {
1173 $options['chatbot_discussions'] = $options['shortcode_chat_discussions'];
1174 unset( $options['shortcode_chat_discussions'] );
1175 $needs_update = true;
1176 }
1177 if ( isset( $options['shortcode_chat_typewriter'] ) ) {
1178 $options['chatbot_typewriter'] = $options['shortcode_chat_typewriter'];
1179 unset( $options['shortcode_chat_typewriter'] );
1180 $needs_update = true;
1181 }
1182 if ( isset( $options['shortcode_chat'] ) ) {
1183 $options['module_chatbots'] = $options['shortcode_chat'];
1184 unset( $options['shortcode_chat'] );
1185 $needs_update = true;
1186 }
1187 if ( isset( $options['openai_usage'] ) ) {
1188 $options['ai_models_usage'] = $options['openai_usage'];
1189 unset( $options['openai_usage'] );
1190 $needs_update = true;
1191 }
1192 #endregion
1193
1194 // The IDs for the embeddings environments are generated here.
1195 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1196 $embeddings_default_exists = false;
1197 if ( isset( $options['embeddings_envs'] ) ) {
1198 foreach ( $options['embeddings_envs'] as &$env ) {
1199 if ( !isset( $env['id'] ) ) {
1200 $env['id'] = $this->get_random_id();
1201 $needs_update = true;
1202 }
1203 if ( $env['id'] === $options['embeddings_default_env'] ) {
1204 $embeddings_default_exists = true;
1205 }
1206 }
1207 }
1208 if ( !$embeddings_default_exists ) {
1209 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1210 $needs_update = true;
1211 }
1212
1213 // The IDs for the AI environments are generated here.
1214 $allEnvIds = [];
1215 $ai_default_exists = false;
1216 if ( isset( $options['ai_envs'] ) ) {
1217 foreach ( $options['ai_envs'] as &$env ) {
1218 if ( !isset( $env['id'] ) ) {
1219 $env['id'] = $this->get_random_id();
1220 $needs_update = true;
1221 }
1222 if ( $env['id'] === $options['ai_default_env'] ) {
1223 $ai_default_exists = true;
1224 }
1225 $allEnvIds[] = $env['id'];
1226 }
1227 }
1228 if ( !$ai_default_exists ) {
1229 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1230 $needs_update = true;
1231 }
1232
1233 // All the models with an envId that does not exist anymore are removed.
1234 if ( isset( $options['ai_models'] ) ) {
1235 $options['ai_models'] = array_values( array_filter( $options['ai_models'],
1236 function( $model ) use ( $allEnvIds, &$needs_update ) {
1237 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1238 $needs_update = true;
1239 return false;
1240 }
1241 return true;
1242 }
1243 ) );
1244 }
1245
1246 if ( $needs_update ) {
1247 ksort( $options );
1248 update_option( $this->option_name, $options, false );
1249 }
1250
1251 return $options;
1252 }
1253
1254 function update_options( $options ) {
1255 if ( !update_option( $this->option_name, $options, false ) ) {
1256 return false;
1257 }
1258 $options = $this->get_all_options( true, true );
1259 return $options;
1260 }
1261
1262 function update_option( $option, $value ) {
1263 $options = $this->get_all_options( true );
1264 $options[$option] = $value;
1265 return $this->update_options( $options );
1266 }
1267
1268 function get_option( $option, $default = null ) {
1269 $options = $this->get_all_options();
1270 return $options[$option] ?? $default;
1271 }
1272
1273 function update_ai_env( $env_id, $option, $value ) {
1274 $options = $this->get_all_options( true );
1275 foreach ( $options['ai_envs'] as &$env ) {
1276 if ( $env['id'] === $env_id ) {
1277 $env[$option] = $value;
1278 break;
1279 }
1280 }
1281 return $this->update_options( $options );
1282 }
1283
1284 function get_engine_models( $engineType ) {
1285 $engines = $this->get_option( 'ai_engines' );
1286 foreach ( $engines as $engine ) {
1287 if ( $engine['type'] === $engineType ) {
1288 return isset( $engine['models'] ) ? $engine['models'] : [];
1289 }
1290 }
1291 return [];
1292 }
1293
1294 function reset_options() {
1295 delete_option( $this->themes_option_name );
1296 delete_option( $this->chatbots_option_name );
1297 delete_option( $this->option_name );
1298 return $this->get_all_options( true );
1299 }
1300 #endregion
1301 }
1302
1303 ?>