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