PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.7.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.7.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 discussion.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
148 // Allow to modify the query before it is sent.
149 // Embedding and Feedback queries are not allowed to be modified.
150 if ( !( $query instanceof Meow_MWAI_Query_Embed ) && !( $query instanceof Meow_MWAI_Query_Feedback ) ) {
151 $query = apply_filters( 'mwai_ai_query', $query );
152 }
153
154 // Let's check the default environment and model.
155 $engine = $this->validate_env_model( $query );
156
157 // Let's run the query.
158 $reply = $engine->run( $query, $streamCallback );
159
160 // Let's allow to modify the reply before it is sent.
161 if ( $markdown ) {
162 if ( $query instanceof Meow_MWAI_Query_Image ) {
163 $reply->result = "";
164 foreach ( $reply->results as $result ) {
165 $reply->result .= "![Image]($result)\n";
166 }
167 }
168 }
169
170 return $reply;
171 }
172
173 public function validate_env_model( $query ) {
174 $envId = !empty( $query->envId ) ? $query->envId : null;
175 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
176 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
177 if ( $query instanceof Meow_MWAI_Query_Text ) {
178 $this->set_default_env_and_model( $query, 'ai_default_env', 'ai_default_model' );
179 }
180 if ( $query instanceof Meow_MWAI_Query_Embed ) {
181 $this->set_default_env_and_model( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
182 }
183 else if ( $query instanceof Meow_MWAI_Query_Image ) {
184 $this->set_default_env_and_model( $query, 'ai_images_default_env', 'ai_images_default_model' );
185 }
186 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
187 $this->set_default_env_and_model( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
188 }
189 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
190 }
191 return $engine;
192 }
193
194 private function set_default_env_and_model( $query, $envOption, $modelOption ) {
195 $defaultEnv = $this->get_option( $envOption );
196 $defaultModel = $this->get_option( $modelOption );
197 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
198 throw new Exception( 'AI Engine: The default environment and model are not set.' );
199 }
200 $query->set_env_id( $defaultEnv );
201 $query->set_model( $defaultModel );
202 }
203 #endregion
204
205 #region Text-Related Helpers
206
207 // Clean the text perfectly, resolve shortcodes, etc, etc.
208 function clean_text( $rawText = "" ) {
209 $text = html_entity_decode( $rawText );
210 $text = wp_strip_all_tags( $text );
211 $text = preg_replace( '/[\r\n]+/', "\n", $text );
212 $text = preg_replace( '/\n+/', "\n", $text );
213 $text = preg_replace( '/\t+/', "\t", $text );
214 return $text . " ";
215 }
216
217 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
218 function clean_sentences( $text, $maxLength = null ) {
219 // Step 1: Identify URLs and replace them with a placeholder.
220 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
221 preg_match_all($urlPattern, $text, $urls);
222 $urlPlaceholders = array();
223 foreach ($urls[0] as $index => $url) {
224 $placeholder = "{urlPlaceholder" . $index . "}";
225 $text = str_replace($url, $placeholder, $text);
226 $urlPlaceholders[$placeholder] = $url;
227 }
228
229 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
230 $sentences = preg_split('/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
231 $hashes = array();
232 $uniqueSentences = array();
233 $total = 0;
234
235 foreach ( $sentences as $sentence ) {
236 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
237 $hash = md5( $sentence );
238 if ( !in_array( $hash, $hashes ) ) {
239 $length = mb_strlen( $sentence, 'UTF-8' );
240 if ( $total + $length > $maxLength ) {
241 continue;
242 }
243 $hashes[] = $hash;
244 $uniqueSentences[] = $sentence;
245 $total += $length;
246 }
247 }
248
249 $freshText = implode( " ", $uniqueSentences );
250
251 // Step 3: Restore URLs in the final text.
252 foreach ($urlPlaceholders as $placeholder => $url) {
253 $freshText = str_replace($placeholder, $url, $freshText);
254 }
255
256 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
257 return $freshText;
258 }
259
260 function get_post_content( $postId ) {
261 $post = get_post( $postId );
262 if ( !$post ) {
263 return false;
264 }
265 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
266 $pattern = '/\[mwai_.*?\]/';
267 $text = preg_replace( $pattern, '', $text );
268 if ( $this->get_option( 'resolve_shortcodes' ) ) {
269 $text = apply_filters( 'the_content', $text );
270 }
271 else {
272 $pattern = "/\[[^\]]+\]/";
273 $text = preg_replace( $pattern, '', $text );
274 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
275 $text = preg_replace( $pattern, '', $text );
276 }
277 $text = $this->clean_text( $text );
278 $text = $this->clean_sentences( $text );
279 $text = apply_filters( 'mwai_post_content', $text, $postId );
280 return $text;
281 }
282
283 function markdown_to_html( $content ) {
284 $Parsedown = new Parsedown();
285 $content = $Parsedown->text( $content );
286 return $content;
287 }
288
289 function get_post_language( $postId ) {
290 $locale = get_locale();
291 $code = strtolower( substr( $locale, 0, 2 ) );
292 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
293 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
294 if ( !empty( $lang ) ) {
295 $locale = $lang['locale'];
296 $humanLanguage = $lang['display_name'];
297 }
298 return strtolower( "$locale ($humanLanguage)" );
299 }
300
301 function do_placeholders( $text ) {
302 $defaultPlaceholders = [];
303 $dataPlaceholders = $this->get_user_data();
304 if ( !empty( $dataPlaceholders ) ) {
305 $defaultPlaceholders = array_merge( $defaultPlaceholders, $dataPlaceholders );
306 }
307 $placeholders = apply_filters( 'mwai_placeholders', $defaultPlaceholders );
308 foreach ( $placeholders as $key => $value ) {
309 $text = str_replace( '{' . $key . '}', $value, $text );
310 }
311 return $text;
312 }
313 #endregion
314
315 #region Image-Related Helpers
316 static function is_image( $file ) {
317 $mimeType = Meow_MWAI_Core::get_mime_type( $file );
318 if ( strpos( $mimeType, 'image' ) !== false ) {
319 return true;
320 }
321 return false;
322 }
323
324 static function get_image_resolution( $url ) {
325 if ( empty( $url ) ) {
326 return null;
327 }
328 $headers = get_headers( $url, 1 );
329 if ( strpos( $headers[0], '200' ) === false ) {
330 return null;
331 }
332 $image_info = getimagesize( $url );
333 if ( $image_info === false ) {
334 return null;
335 }
336 return [
337 'width' => $image_info[0],
338 'height' => $image_info[1]
339 ];
340 }
341
342 static function get_mime_type( $file ) {
343 $mimeType = null;
344
345 // Let's try to use mime_content_type if the function exists
346 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
347 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
348 $mimeType = mime_content_type( $file );
349 }
350
351 // Otherwise, let's check the file extension (which can actually also be an URL)
352 if ( !$mimeType ) {
353 $extension = pathinfo( $file, PATHINFO_EXTENSION );
354 $extension = strtolower( $extension );
355 $mimeTypes = [
356 'jpg' => 'image/jpeg',
357 'jpeg' => 'image/jpeg',
358 'png' => 'image/png',
359 'gif' => 'image/gif',
360 'webp' => 'image/webp',
361 'bmp' => 'image/bmp',
362 'tiff' => 'image/tiff',
363 'tif' => 'image/tiff',
364 'svg' => 'image/svg+xml',
365 'ico' => 'image/x-icon',
366 'pdf' => 'application/pdf',
367 ];
368 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
369 }
370
371 return $mimeType;
372 }
373
374 function download_image( $url ) {
375 $args = array( 'timeout' => 60, );
376 $response = wp_safe_remote_get( $url, $args );
377 if ( is_wp_error( $response ) ) {
378 throw new Exception( $response->get_error_message() );
379 }
380 $output = wp_remote_retrieve_body( $response );
381 if ( is_wp_error( $output ) ) {
382 throw new Exception( $output->get_error_message() );
383 }
384 return $output;
385 }
386
387 /**
388 * Add an image from a URL to the Media Library.
389 * @param string $url The URL of the image to be downloaded.
390 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
391 * @param string $title The title of the image.
392 * @param string $description The description of the image.
393 * @param string $caption The caption of the image.
394 * @param string $alt The alt text of the image.
395 * @return int The attachment ID of the image.
396 */
397 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null ) {
398 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
399 $url_filename = $path_parts['basename'];
400 $file_type = wp_check_filetype( $url_filename, null );
401 $allowed_types = get_allowed_mime_types();
402 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
403 throw new Exception( 'Invalid file type from URL.' );
404 }
405
406 // Initial extension from URL file name
407 $extension = $file_type['ext'];
408
409 if ( !empty( $filename ) ) {
410 $custom_file_type = wp_check_filetype( $filename, null );
411 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
412 throw new Exception( 'Invalid custom file type.' );
413 }
414 // Use the extension from the custom filename if valid
415 $extension = $custom_file_type['ext'];
416 }
417
418 $image_data = $this->download_image( $url );
419 if ( !$image_data ) {
420 throw new Exception( 'Could not download the image.' );
421 }
422 $upload_dir = wp_upload_dir();
423
424 // Filename handling including 'generated_' prefix scenario
425 if ( empty( $filename ) ) {
426 $filename = sanitize_file_name( $url_filename );
427 if ( empty( $extension ) ) { // This condition might now be redundant
428 $extension = $file_type['ext'];
429 }
430 // Filename length check and prepend if conditions met
431 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
432 $filename = $this->get_random_id( 16 ) . '.' . $extension;
433 }
434 if ( strpos( $filename, '.' ) === false ) {
435 $filename .= '.' . $extension;
436 }
437 }
438
439 // Directory and file path handling
440 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
441 $file = $upload_dir['path'] . '/' . $filename;
442 }
443 else {
444 $file = $upload_dir['basedir'] . '/' . $filename;
445 }
446
447 // Ensure file name uniqueness in the directory
448 $i = 1;
449 $parts = pathinfo( $file );
450 while ( file_exists( $file ) ) {
451 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
452 $i++;
453 }
454
455 // Writing the file to disk
456 file_put_contents( $file, $image_data );
457
458 // Attachment and metadata handling in WP
459 $attachment = [
460 'post_mime_type' => $file_type['type'],
461 'post_title' => $title ?? '',
462 'post_content' => $description ?? '',
463 'post_excerpt' => $caption ?? '',
464 'post_status' => 'inherit'
465 ];
466 $attachmentId = wp_insert_attachment( $attachment, $file );
467 require_once( ABSPATH . 'wp-admin/includes/image.php' );
468 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
469 wp_update_attachment_metadata( $attachmentId, $attachment_data );
470 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
471
472 // Attach the image to a post if needed
473 if ( $attachedPost ) {
474 wp_update_post( [ 'ID' => $attachmentId, 'post_parent' => $attachedPost ] );
475 }
476
477 return $attachmentId;
478 }
479 #endregion
480
481 #region Context-Related Helpers
482 function retrieve_context( $params, $query ) {
483 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
484 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
485 $context = apply_filters( 'mwai_context_search', [], $query, [
486 'embeddingsEnvId' => $embeddingsEnvId
487 ]);
488 if ( empty( $context ) ) {
489 return null;
490 }
491 else if ( !isset( $context['content'] ) ) {
492 Meow_MWAI_Logging::warn( "A context without content was returned." );
493 return null;
494 }
495 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
496 $context['length'] = strlen( $context['content'] );
497 return $context;
498 }
499 #endregion
500
501 #region Users/Sessions Helpers
502
503 function get_nonce( $force = false ) {
504 if ( !$force && !is_user_logged_in() ) {
505 return null;
506 }
507 if ( isset( $this->nonce ) ) {
508 return $this->nonce;
509 }
510 $this->nonce = wp_create_nonce( 'wp_rest' );
511 return $this->nonce;
512 }
513
514 // This is a bit hacky, but chatId needs to be retrieved or generated.
515 // Maybe we can clean this up later.
516 function fix_chat_id( $query, $params ) {
517 if ( isset( $query->chatId ) && $query->chatId !== 'N/A' ) {
518 return $query->chatId;
519 }
520 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : $query->session;
521 if ( $chatId === 'N/A' ) {
522 $chatId = $this->get_random_id( 8 );
523 }
524 $query->set_chat_id( $chatId );
525 return $chatId;
526 }
527
528 function get_session_id() {
529 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
530 return $_COOKIE['mwai_session_id'];
531 }
532 return "N/A";
533 }
534
535 // Get the UserID from the data, or from the current user
536 function get_user_id( $data = null ) {
537 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
538 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
539 $admin = get_users( [ 'role' => 'administrator' ] );
540 if ( !empty( $admin ) ) {
541 return $admin[0]->ID;
542 }
543 }
544 if ( isset( $data ) && isset( $data['userId'] ) ) {
545 return (int)$data['userId'];
546 }
547 if ( is_user_logged_in() ) {
548 $current_user = wp_get_current_user();
549 if ( $current_user->ID > 0 ) {
550 return $current_user->ID;
551 }
552 }
553 return null;
554 }
555
556 function get_admin_user() {
557 $admin = get_users( [ 'role' => 'administrator' ] );
558 if ( !empty( $admin ) ) {
559 return $admin[0];
560 }
561 return null;
562 }
563
564 function get_user_data() {
565 $user = wp_get_current_user();
566 if ( empty( $user ) || empty( $user->ID ) ) {
567 return null;
568 }
569 $placeholders = array(
570 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
571 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
572 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
573 $user->data->user_login : null,
574 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
575 $user->data->display_name : null,
576 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
577 );
578 return $placeholders;
579 }
580
581 function get_ip_address( $force = false ) {
582 $ip = '127.0.0.1';
583 $headers = [
584 'HTTP_TRUE_CLIENT_IP',
585 'HTTP_CF_CONNECTING_IP',
586 'HTTP_X_REAL_IP',
587 'HTTP_CLIENT_IP',
588 'HTTP_X_FORWARDED_FOR',
589 'HTTP_X_FORWARDED',
590 'HTTP_X_CLUSTER_CLIENT_IP',
591 'HTTP_FORWARDED_FOR',
592 'HTTP_FORWARDED',
593 'REMOTE_ADDR',
594 ];
595
596 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
597 $ip = ( string )$params[ 'ip' ];
598 }
599 else {
600 foreach ( $headers as $header ) {
601 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
602 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
603 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
604 break;
605 }
606 }
607 }
608
609 $ip = filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
610
611 // If privacy_first is enabled, we hash the IP address.
612 if ( !$force && $this->get_option( 'privacy_first' ) && !empty( $ip ) ) {
613 $hash = hash( 'sha256', $ip, true ); // binary output
614 $ip = substr( rtrim( strtr( base64_encode( $hash ), '+/', '-_'), '=' ), 0, 12 );
615 }
616
617 return $ip;
618 }
619
620 #endregion
621
622 #region Sanitization
623 function sanitize_sort( &$sort, $default_accessor = 'created', $default_order = 'DESC',
624 $allowed_columns = array( 'created', 'updated', 'name', 'id', 'time', 'units', 'price' )) {
625
626 // Ensure $sort is an array
627 if ( !is_array( $sort ) ) {
628 $sort = [ "accessor" => $default_accessor, "by" => $default_order ];
629 }
630 // Extract and sanitize the accessor
631 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
632 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
633 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
634 $sort_accessor = $default_accessor;
635 }
636 // Extract and sanitize the sort order
637 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
638 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
639 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
640 $sort_by = $default_order;
641 }
642 // Update the sort array with sanitized values
643 $sort['accessor'] = $sort_accessor;
644 $sort['by'] = $sort_by;
645 }
646 #endregion
647
648 #region Other Helpers
649 function safe_strlen( $string, $encoding = 'UTF-8' ) {
650 if ( function_exists( 'mb_strlen' ) ) {
651 return mb_strlen( $string, $encoding );
652 }
653 else {
654 // Fallback implementation for environments without mbstring extension
655 return preg_match_all( '/./u', $string, $matches );
656 }
657 }
658
659 public function check_rest_nonce( $request ) {
660 $nonce = $request->get_header( 'X-WP-Nonce' );
661 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
662 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
663 }
664
665 function get_random_id( $length = 8, $excludeIds = [] ) {
666 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
667 $charactersLength = strlen( $characters );
668 $randomId = '';
669 for ( $i = 0; $i < $length; $i++ ) {
670 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
671 }
672 if ( in_array( $randomId, $excludeIds ) ) {
673 return $this->get_random_id( $length, $excludeIds );
674 }
675 return $randomId;
676 }
677
678 function is_url( $url ) {
679 return strpos( $url, 'http' ) === 0 ? true : false;
680 }
681
682 function get_post_types() {
683 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
684 $post_types = array();
685 $types = get_post_types( [], 'objects' );
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 foreach ( $types as $type ) {
692 $forced = in_array( $type->name, $syncPostTypes );
693 // Should not be excluded.
694 if ( !$forced && in_array( $type->name, $excluded ) ) {
695 continue;
696 }
697 // Should be public.
698 if ( !$forced && !$type->public ) {
699 continue;
700 }
701 $post_types[] = array(
702 'name' => $type->labels->name,
703 'type' => $type->name,
704 );
705 }
706
707 // Let's get the Post Types that are enabled for Embeddings Sync
708 $embeddingsSettings = $this->get_option( 'embeddings' );
709 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
710
711 return $post_types;
712 }
713
714 function get_post( $post ) {
715 if ( is_numeric( $post ) ) {
716 $post = get_post( $post );
717 }
718 if ( is_object( $post ) ) {
719 $post = (array)$post;
720 }
721 if ( !is_array( $post ) ) {
722 return null;
723 }
724 $language = $this->get_post_language( $post['ID'] );
725 $content = $this->get_post_content( $post['ID'] );
726 $title = $post['post_title'];
727 $excerpt = $post['post_excerpt'];
728 $url = get_permalink( $post['ID'] );
729 $checksum = wp_hash( $content . $title . $url );
730 return [
731 'postId' => (int)$post['ID'],
732 'title' => $title,
733 'content' => $content,
734 'excerpt' => $excerpt,
735 'url' => $url,
736 'language' => $language ?? 'english',
737 'checksum' => $checksum,
738 ];
739 }
740 #endregion
741
742 #region Usage & Costs
743
744 // Quick and dirty token estimation
745 // Let's keep this synchronized with Helpers in JS
746 static function estimate_tokens( ...$args ): int {
747 $text = "";
748 foreach ( $args as $arg ) {
749 if ( is_array( $arg ) ) {
750 foreach ( $arg as $message ) {
751 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
752 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
753 }
754 }
755 else if ( is_string( $arg ) ) {
756 $text .= $arg;
757 }
758 }
759 $averageTokenLength = 4;
760 $words = preg_split( '/\s+/', trim( $text ) );
761 $tokenCount = 0;
762 foreach ( $words as $word ) {
763 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
764 }
765 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
766 }
767
768 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
769 if ( !is_numeric( $in_tokens ) ) {
770 throw new Exception( 'AI Engine: in_tokens must be a number.' );
771 }
772 if ( !is_numeric( $out_tokens ) ) {
773 $out_tokens = 0;
774 }
775 if ( !$model ) {
776 throw new Exception( 'AI Engine: model is required.' );
777 }
778 $usage = $this->get_option( 'ai_models_usage' );
779 $month = date( 'Y-m' );
780 if ( !isset( $usage[$month] ) ) {
781 $usage[$month] = array();
782 }
783 if ( !isset( $usage[$month][$model] ) ) {
784 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
785 }
786 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
787 $usage[$month][$model]['completion_tokens'] += $out_tokens;
788 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
789 $this->update_option( 'ai_models_usage', $usage );
790 $usageInfo = [
791 'prompt_tokens' => $in_tokens,
792 'completion_tokens' => $out_tokens,
793 'total_tokens' => $in_tokens + $out_tokens,
794 ];
795 if ( $returned_price !== null ) {
796 $usageInfo['price'] = $returned_price;
797 }
798 return $usageInfo;
799 }
800
801 public function record_audio_usage( $model, $seconds ) {
802 if ( !is_numeric( $seconds ) ) {
803 throw new Exception( 'AI Engine: seconds must be a number.' );
804 }
805 if ( !$model ) {
806 throw new Exception( 'AI Engine: model is required.' );
807 }
808 $usage = $this->get_option( 'ai_models_usage' );
809 $month = date( 'Y-m' );
810 if ( !isset( $usage[$month] ) ) {
811 $usage[$month] = array();
812 }
813 if ( !isset( $usage[$month][$model] ) ) {
814 $usage[$month][$model] = array( 'seconds' => 0 );
815 }
816 $usage[$month][$model]['seconds'] += $seconds;
817 $this->update_option( 'ai_models_usage', $usage );
818 return [ 'seconds' => $seconds ];
819 }
820
821 public function record_images_usage( $model, $resolution, $images ) {
822 if ( !$model || !$resolution || !$images ) {
823 throw new Exception( 'Missing parameters for record_image_usage.' );
824 }
825 $usage = $this->get_option( 'ai_models_usage' );
826 $month = date( 'Y-m' );
827 if ( !isset( $usage[$month] ) ) {
828 $usage[$month] = array();
829 }
830 if ( !isset( $usage[$month][$model] ) ) {
831 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
832 }
833 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
834 $usage[$month][$model]['resolution'][$resolution] = 0;
835 }
836 $usage[$month][$model]['resolution'][$resolution] += $images;
837 $usage[$month][$model]['images'] += $images;
838 $this->update_option( 'ai_models_usage', $usage );
839 return [ 'resolution' => $resolution, 'images' => $images ];
840 }
841
842 #endregion
843
844 #region Streaming
845 public function stream_push( $data, $query = null ) {
846 $data = apply_filters( 'mwai_stream_push', $data, $query );
847 $out = "data: " . json_encode( $data );
848 echo $out;
849 echo "\n\n";
850 if ( ob_get_level() > 0 ) {
851 ob_end_flush();
852 }
853 flush();
854 }
855 #endregion
856
857 #region Options
858 function get_themes() {
859 $themes = get_option( $this->themes_option_name, [] );
860 $themes = empty( $themes ) ? [] : $themes;
861
862 $internalThemes = [
863 'chatgpt' => [
864 'type' => 'internal','name' => 'ChatGPT', 'themeId' => 'chatgpt',
865 'settings' => [], 'style' => ""
866 ],
867 'messages' => [
868 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
869 'settings' => [], 'style' => ""
870 ],
871 'timeless' => [
872 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
873 'settings' => [], 'style' => ""
874 ],
875 ];
876 $customThemes = [];
877 foreach ( $themes as $theme ) {
878 if ( isset( $internalThemes[$theme['themeId']] ) ) {
879 $internalThemes[$theme['themeId']] = $theme;
880 continue;
881 }
882 $customThemes[] = $theme;
883 }
884 return array_merge(array_values($internalThemes), $customThemes);
885 }
886
887 function update_themes( $themes ) {
888 update_option( $this->themes_option_name, $themes );
889 return $themes;
890 }
891
892 function get_chatbots() {
893 $chatbots = get_option( $this->chatbots_option_name, [] );
894 $hasChanges = false;
895 if ( empty( $chatbots ) ) {
896 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
897 }
898 $hasDefault = false;
899 foreach ( $chatbots as &$chatbot ) {
900 if ( $chatbot['botId'] === 'default' ) {
901 $hasDefault = true;
902 }
903 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
904 // Use default value if not set.
905 if ( !isset( $chatbot[$key] ) ) {
906 $chatbot[$key] = $value;
907 }
908 }
909
910 /*
911 This is the best section to rename fields.
912 We did this in 2024 for context to instructions, and fileUpload to fileSearch. fileSearch is for assistant file search, and fileUpload is now for chatbot file upload (similar to vision, but for files instead of images).
913 */
914
915 // if ( isset( $chatbot['context'] ) ) {
916 // $chatbot['instructions'] = $chatbot['context'];
917 // unset( $chatbot['context'] );
918 // $hasChanges = true;
919 // }
920 }
921 if ( !$hasDefault ) {
922 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
923 array_unshift( $chatbots, $defaultBot );
924 $hasChanges = true;
925 }
926 if ( $hasChanges ) {
927 update_option( $this->chatbots_option_name, $chatbots );
928 }
929 return $chatbots;
930 }
931
932 function get_chatbot( $botId ) {
933 $chatbots = $this->get_chatbots();
934 foreach ( $chatbots as $chatbot ) {
935 if ( $chatbot['botId'] === (string)$botId ) {
936 return $chatbot;
937 }
938 }
939 return null;
940 }
941
942 function get_embeddings_env( $envId ) {
943 $envs = $this->get_option( 'embeddings_envs' );
944 foreach ( $envs as $env ) {
945 if ( $env['id'] === $envId ) {
946 return $env;
947 }
948 }
949 return null;
950 }
951
952 function get_ai_env( $envId ) {
953 $envs = $this->get_option( 'ai_envs' );
954 foreach ( $envs as $env ) {
955 if ( $env['id'] === $envId ) {
956 return $env;
957 }
958 }
959 return null;
960 }
961
962 function get_assistant( $envId, $assistantId ) {
963 $env = $this->get_ai_env( $envId );
964 if ( !$env ) {
965 return null;
966 }
967 $assistants = $env['assistants'];
968 foreach ( $assistants as $assistant ) {
969 if ( $assistant['id'] === $assistantId ) {
970 return $assistant;
971 }
972 }
973 return null;
974 }
975
976 function get_theme( $themeId ) {
977 $themes = $this->get_themes();
978 foreach ( $themes as $theme ) {
979 if ( $theme['themeId'] === $themeId ) {
980 return $theme;
981 }
982 }
983 return null;
984 }
985
986 function update_chatbots( $chatbots ) {
987 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
988 // TODO: I think some HTML fields are missing, guestName, maybe others.
989 $htmlFields = [ 'instructions', 'textCompliance', 'aiName', 'userName', 'startSentence' ];
990 $keepLineReturnsFields = [ 'instructions' ];
991 $whiteSpacedFields = [ 'context' ];
992 foreach ( $chatbots as &$chatbot ) {
993 foreach ( $chatbot as $key => &$value ) {
994 if ( in_array( $key, $deprecatedFields ) ) {
995 unset( $chatbot[$key] );
996 continue;
997 }
998 if ( in_array( $key, $htmlFields ) ) {
999 $value = wp_kses_post( $value );
1000 }
1001 else if ( in_array( $key, $whiteSpacedFields ) ) {
1002 $value = sanitize_textarea_field( $value );
1003 }
1004 else if ( $key === 'functions' ) {
1005 $functions = [];
1006 foreach ( $value as $function ) {
1007 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
1008 $functions[] = [
1009 'id' => sanitize_text_field( $function['id'] ),
1010 'type' => sanitize_text_field( $function['type'] ),
1011 ];
1012 }
1013 }
1014 $value = $functions;
1015 }
1016 else {
1017 if ( in_array( $key, $keepLineReturnsFields ) ) {
1018 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
1019 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
1020 }
1021 $value = sanitize_text_field( $value );
1022 if ( in_array( $key, $keepLineReturnsFields ) ) {
1023 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1024 }
1025 }
1026 }
1027 }
1028 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1029 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1030 $chatbots = get_option( $this->chatbots_option_name, [] );
1031 return $chatbots;
1032 }
1033 return $chatbots;
1034 }
1035
1036 function populate_dynamic_options( $options ) {
1037 // Languages
1038 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1039
1040 // Consolidate the Engines and their Models
1041 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1042 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1043 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1044 foreach ( $options['ai_engines'] as &$engine ) {
1045 if ( $engine['type'] === 'openai' ) {
1046 $engine['models'] = apply_filters( 'mwai_openai_models',
1047 Meow_MWAI_Engines_OpenAI::get_models_static()
1048 );
1049 }
1050 else if ( $engine['type'] === 'anthropic' ) {
1051 $engine['models'] = apply_filters( 'mwai_anthropic_models',
1052 Meow_MWAI_Engines_Anthropic::get_models_static()
1053 );
1054 }
1055 else if ( $engine['type'] === 'perplexity' ) {
1056 $engine['models'] = apply_filters( 'mwai_perplexity_models',
1057 Meow_MWAI_Engines_Perplexity::get_models_static()
1058 );
1059 }
1060 else {
1061 $engine['models'] = [];
1062 foreach ( $options['ai_models'] as $model ) {
1063 if ( $model['type'] === $engine['type'] ) {
1064 $engine['models'][] = $model;
1065 }
1066 }
1067 }
1068 }
1069
1070 // Functions via Code Engine (or custom code)
1071 $json = [];
1072 $functions = apply_filters( 'mwai_functions_list', [] );
1073 foreach ( $functions as $function ) {
1074 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1075 }
1076 $options['functions'] = $json;
1077
1078 // Addons
1079 $options['addons'] = apply_filters( 'mwai_addons', [
1080 [
1081 "slug" => "mwai-notifications",
1082 "name" => "Notifications",
1083 "description" => "Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.",
1084 "install_url" => "https://meowapps.com/products/mwai-notifications/",
1085 "settings_url" => null,
1086 "stars" => 4,
1087 "enabled" => false
1088 ],
1089 [
1090 "slug" => "mwai-ollama",
1091 "name" => "Ollama",
1092 "description" => "Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.",
1093 "install_url" => "https://meowapps.com/products/mwai-ollama/",
1094 "settings_url" => null,
1095 "stars" => 3,
1096 "enabled" => false
1097 ],
1098 [
1099 "slug" => "mwai-deepseek",
1100 "name" => "DeepSeek",
1101 "description" => "Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.",
1102 "install_url" => "https://meowapps.com/products/deepseek/",
1103 "settings_url" => null,
1104 "stars" => 3,
1105 "enabled" => false
1106 ],
1107 [
1108 "slug" => "mwai-websearch",
1109 "name" => "Web Search",
1110 "description" => "Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.",
1111 "install_url" => "https://meowapps.com/products/mwai-websearch/",
1112 "settings_url" => null,
1113 "stars" => 5,
1114 "enabled" => false
1115 ],
1116 [
1117 "slug" => "mwai-better-links",
1118 "name" => "Better Links",
1119 "description" => "Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.",
1120 "install_url" => "https://meowapps.com/products/mwai-better-links/",
1121 "settings_url" => null,
1122 "stars" => 3,
1123 "enabled" => false
1124 ],
1125 [
1126 "slug" => "mwai-woo-basics",
1127 "name" => "Woo Basics",
1128 "description" => "Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.",
1129 "install_url" => "https://meowapps.com/products/mwai-woo-basics/",
1130 "settings_url" => null,
1131 "stars" => 2,
1132 "enabled" => false
1133 ],
1134 [
1135 "slug" => "mwai-quick-actions",
1136 "name" => "Quick Actions",
1137 "description" => "Enable dynamic quick actions at chat start or during events, helping users find what they need faster.",
1138 "install_url" => "https://meowapps.com/products/mwai-quick-actions/",
1139 "settings_url" => null,
1140 "stars" => 3,
1141 "enabled" => false
1142 ],
1143 [
1144 "slug" => "mwai-content-parser",
1145 "name" => "Content Parser",
1146 "description" => "Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.",
1147 "install_url" => "https://meowapps.com/products/mwai-content-parser/",
1148 "settings_url" => null,
1149 "stars" => 2,
1150 "enabled" => false
1151 ],
1152 [
1153 "slug" => "mwai-visitor-form",
1154 "name" => "Visitor Form",
1155 "description" => "Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.",
1156 "install_url" => "https://meowapps.com/products/mwai-visitor-form/",
1157 "settings_url" => null,
1158 "stars" => 2,
1159 "enabled" => false
1160 ],
1161 [
1162 "slug" => "mwai-dynamic-keys",
1163 "name" => "Dynamic Keys",
1164 "description" => "Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.",
1165 "install_url" => "https://meowapps.com/products/mwai-dynamic-keys/",
1166 "settings_url" => null,
1167 "stars" => 1,
1168 "enabled" => false
1169 ],
1170 ] );
1171
1172 return $options;
1173 }
1174
1175 function get_all_options( $force = false, $sanitize = false ) {
1176 if ( $force || is_null( $this->options ) ) {
1177 $options = get_option( $this->option_name, [] );
1178 $init_mode = empty( $options );
1179 foreach ( MWAI_OPTIONS as $key => $value ) {
1180 if ( !isset( $options[$key] ) ) {
1181 $options[$key] = $value;
1182 }
1183 }
1184 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1185 $options['default_limits'] = MWAI_LIMITS;
1186 if ( $sanitize || $init_mode ) {
1187 $options = $this->sanitize_options( $options );
1188 }
1189 $this->options = $options;
1190 }
1191 $options = $this->populate_dynamic_options( $this->options );
1192 return $options;
1193 }
1194
1195 // Sanitize options when we update the plugin or perform some updates
1196 // if we change the structure of the options.
1197 function sanitize_options( $options ) {
1198 $needs_update = false;
1199
1200 // Removing old options of options renaming should be done here, as it was done before.
1201 // Check version 2.6.8 for an example.
1202
1203 // Avoid the logs_path to be a PHP file.
1204 if ( isset( $options['logs_path'] ) ) {
1205 $logs_path = $options['logs_path'];
1206 if ( substr( $logs_path, -4 ) !== '.log' ) {
1207 $options['logs_path'] = '';
1208 $needs_update = true;
1209 }
1210 }
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 ?>