PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.3
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / core.php
ai-engine / classes Last commit date
engines 1 year ago modules 1 year ago queries 1 year ago admin.php 1 year ago api.php 1 year ago core.php 1 year ago discussion.php 1 year ago event.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
1409 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 public $search = null;
30
31 public function __construct() {
32 Meow_MWAI_Logging::init( 'mwai_options', 'AI Engine' );
33 $this->site_url = get_site_url();
34 $this->is_rest = MeowCommon_Helpers::is_rest();
35 $this->is_cli = defined( 'WP_CLI' );
36 $this->files = new Meow_MWAI_Modules_Files( $this );
37 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
38
39 add_action( 'plugins_loaded', array( $this, 'init' ) );
40 add_action( 'wp_register_script', array( $this, 'register_scripts' ) );
41 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
42 add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
43 }
44
45 #region Init & Scripts
46 function init() {
47 global $mwai;
48 $this->chatbot = null;
49 $this->discussions = null;
50 new Meow_MWAI_Modules_Security( $this );
51
52 // REST API
53 if ( $this->is_rest ) {
54 new Meow_MWAI_Rest( $this );
55 }
56
57 // WP Admin
58 if ( is_admin() ) {
59 new Meow_MWAI_Admin( $this );
60 }
61
62 // GDPR Module
63 if ( $this->get_option( 'chatbot_gdpr_consent' ) ) {
64 new Meow_MWAI_Modules_GDPR( $this );
65 }
66
67 // Suggestions Module
68 if ( $this->get_option( 'module_suggestions' ) && ( is_admin() || $this->is_rest ) ) {
69 $this->magicWand = new Meow_MWAI_Modules_Wand( $this );
70 }
71
72 // Administrator in WP Admin
73 if ( is_admin() && current_user_can( 'manage_options' ) ) {
74 $module_advisor = $this->get_option( 'module_advisor' );
75 if ( $module_advisor ) {
76 new Meow_MWAI_Modules_Advisor( $this );
77 }
78 }
79
80 // Chatbots & Discussions
81 if ( $this->get_option( 'module_chatbots' ) ) {
82 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
83 $this->discussions = new Meow_MWAI_Modules_Discussions();
84 }
85
86 // Search
87 if ( $this->get_option( 'module_search' ) ) {
88 $this->search = new Meow_MWAI_Modules_Search( $this );
89 }
90
91 // Advanced Core
92 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
93 new MeowPro_MWAI_Core( $this );
94 }
95
96 // Simple API
97 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
98
99 // MCP
100 if ( $this->get_option( 'module_mcp' ) ) {
101 new Meow_MWAI_Labs_MCP( $this );
102
103 // Core - Core WordPress MCP tools
104 if ( $this->get_option( 'mcp_core' ) ) {
105 new Meow_MWAI_Labs_MCP_Core( $this );
106 }
107
108 // Dynamic REST - WordPress REST API MCP tools
109 if ( $this->get_option( 'mcp_dynamic_rest' ) ) {
110 require_once MWAI_PATH . '/labs/mcp_rest.php';
111 new Meow_MWAI_Labs_MCP_Rest();
112 }
113
114 // Themes - Pro theme management MCP tools
115 if ( $this->get_option( 'mcp_themes' ) && class_exists( 'MeowPro_MWAI_MCP_Theme' ) ) {
116 new MeowPro_MWAI_MCP_Theme( $this );
117 }
118
119 // Plugins - Pro plugin management MCP tools
120 if ( $this->get_option( 'mcp_plugins' ) && class_exists( 'MeowPro_MWAI_MCP_Plugin' ) ) {
121 new MeowPro_MWAI_MCP_Plugin( $this );
122 }
123 }
124 }
125
126 public function register_scripts() {
127 // Register Highlight.js
128 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
129 // Register CSS for the themes
130 $themes = $this->get_themes();
131 foreach ( $themes as $theme ) {
132 if ( $theme['type'] === 'internal' ) {
133 $themeId = $theme['themeId'];
134 $filename = $themeId . '.css';
135 $physical_file = trailingslashit( MWAI_PATH ) . 'themes/' . $filename;
136 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
137 wp_register_style( 'mwai_chatbot_theme_' . $themeId, trailingslashit( MWAI_URL )
138 . 'themes/' . $filename, [], $cache_buster );
139 }
140 }
141 }
142
143 public function enqueue_theme( $themeId ) {
144 if ( empty( $themeId ) ) {
145 return;
146 }
147 wp_enqueue_style( "mwai_chatbot_theme_$themeId" );
148 }
149
150 public function enqueue_themes() {
151 $themes = $this->get_themes();
152 foreach ( $themes as $theme ) {
153 if ( $theme['type'] === 'internal' ) {
154 $this->enqueue_theme( $theme['themeId'] );
155 }
156 }
157 }
158
159 #endregion
160
161 #region Roles & Capabilities
162 function can_start_session() {
163 return apply_filters( 'mwai_allow_session', true );
164 }
165
166 function can_access_settings() {
167 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
168 }
169
170 function can_access_features() {
171 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
172 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
173 }
174
175 function can_access_public_api( $feature, $extra ) {
176 $logged_in = is_user_logged_in();
177 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
178 }
179 #endregion
180
181 #region AI-Related Helpers
182 function run_query( $query, $streamCallback = null, $markdown = false ) {
183
184 // Allow to modify the query before it is sent.
185 // Embedding and Feedback queries are not allowed to be modified.
186 if ( !( $query instanceof Meow_MWAI_Query_Embed ) && !( $query instanceof Meow_MWAI_Query_Feedback ) ) {
187 $query = apply_filters( 'mwai_ai_query', $query );
188 }
189
190 // Let's check the default environment and model.
191 $engine = $this->validate_env_model( $query );
192
193 // Let's run the query.
194 $reply = $engine->run( $query, $streamCallback );
195
196 // Let's allow to modify the reply before it is sent.
197 if ( $markdown ) {
198 if ( $query instanceof Meow_MWAI_Query_Image || $query instanceof Meow_MWAI_Query_EditImage ) {
199 $reply->result = "";
200 foreach ( $reply->results as $result ) {
201 $reply->result .= "![Image]($result)\n";
202 }
203 }
204 }
205
206 return $reply;
207 }
208
209 public function validate_env_model( $query ) {
210 $envId = !empty( $query->envId ) ? $query->envId : null;
211 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
212
213 if ( empty( $envId ) || !$engine->retrieve_model_info( $query->model ) ) {
214 if ( !empty( $envId ) ) {
215 $error = sprintf( 'The model %s is not available in the environment %s.', $query->model, $envId );
216 Meow_MWAI_Logging::warn( $error );
217 }
218 if ( $query instanceof Meow_MWAI_Query_Text ) {
219 $this->set_default_env_and_model( $query, 'ai_default_env', 'ai_default_model' );
220 }
221 if ( $query instanceof Meow_MWAI_Query_Embed ) {
222 $this->set_default_env_and_model( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
223 }
224 else if ( $query instanceof Meow_MWAI_Query_Image || $query instanceof Meow_MWAI_Query_EditImage ) {
225 $this->set_default_env_and_model( $query, 'ai_images_default_env', 'ai_images_default_model' );
226 }
227 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
228 $this->set_default_env_and_model( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
229 }
230 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
231 }
232 return $engine;
233 }
234
235 private function set_default_env_and_model( $query, $envOption, $modelOption ) {
236 $defaultEnv = $this->get_option( $envOption );
237 $defaultModel = $this->get_option( $modelOption );
238 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
239 throw new Exception( 'AI Engine: The default environment and model are not set.' );
240 }
241 $query->set_env_id( $defaultEnv );
242 $query->set_model( $defaultModel );
243 }
244 #endregion
245
246 #region Text-Related Helpers
247
248 // Clean the text perfectly, resolve shortcodes, etc, etc.
249 function clean_text( $rawText = "" ) {
250 $text = html_entity_decode( $rawText );
251 $text = wp_strip_all_tags( $text );
252 $text = preg_replace( '/[\r\n]+/', "\n", $text );
253 $text = preg_replace( '/\n+/', "\n", $text );
254 $text = preg_replace( '/\t+/', "\t", $text );
255 return $text . " ";
256 }
257
258 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
259 function clean_sentences( $text, $maxLength = null ) {
260 // Step 1: Identify URLs and replace them with a placeholder.
261 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
262 preg_match_all($urlPattern, $text, $urls);
263 $urlPlaceholders = array();
264 foreach ($urls[0] as $index => $url) {
265 $placeholder = "{urlPlaceholder" . $index . "}";
266 $text = str_replace($url, $placeholder, $text);
267 $urlPlaceholders[$placeholder] = $url;
268 }
269
270 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
271 $sentences = preg_split('/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
272 $hashes = array();
273 $uniqueSentences = array();
274 $total = 0;
275
276 foreach ( $sentences as $sentence ) {
277 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
278 $hash = md5( $sentence );
279 if ( !in_array( $hash, $hashes ) ) {
280 $length = mb_strlen( $sentence, 'UTF-8' );
281 if ( $total + $length > $maxLength ) {
282 continue;
283 }
284 $hashes[] = $hash;
285 $uniqueSentences[] = $sentence;
286 $total += $length;
287 }
288 }
289
290 $freshText = implode( " ", $uniqueSentences );
291
292 // Step 3: Restore URLs in the final text.
293 foreach ($urlPlaceholders as $placeholder => $url) {
294 $freshText = str_replace($placeholder, $url, $freshText);
295 }
296
297 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
298 return $freshText;
299 }
300
301 function get_post_content( $postId ) {
302 $post = get_post( $postId );
303 if ( !$post ) {
304 return false;
305 }
306 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
307 $pattern = '/\[mwai_.*?\]/';
308 $text = preg_replace( $pattern, '', $text );
309 if ( $this->get_option( 'resolve_shortcodes' ) ) {
310 $text = apply_filters( 'the_content', $text );
311 }
312 else {
313 $pattern = "/\[[^\]]+\]/";
314 $text = preg_replace( $pattern, '', $text );
315 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
316 $text = preg_replace( $pattern, '', $text );
317 }
318 $text = $this->clean_text( $text );
319 $text = $this->clean_sentences( $text );
320 $text = apply_filters( 'mwai_post_content', $text, $postId );
321 return $text;
322 }
323
324 function markdown_to_html( $content ) {
325 $Parsedown = new Parsedown();
326 $content = $Parsedown->text( $content );
327 return $content;
328 }
329
330 function get_post_language( $postId ) {
331 $locale = get_locale();
332 $code = strtolower( substr( $locale, 0, 2 ) );
333 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
334 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
335 if ( !empty( $lang ) ) {
336 $locale = $lang['locale'];
337 $humanLanguage = $lang['display_name'];
338 }
339 return strtolower( "$locale ($humanLanguage)" );
340 }
341
342 function do_placeholders( $text ) {
343 $defaultPlaceholders = [];
344 $dataPlaceholders = $this->get_user_data();
345 if ( !empty( $dataPlaceholders ) ) {
346 $defaultPlaceholders = array_merge( $defaultPlaceholders, $dataPlaceholders );
347 }
348 $placeholders = apply_filters( 'mwai_placeholders', $defaultPlaceholders );
349 foreach ( $placeholders as $key => $value ) {
350 $text = str_replace( '{' . $key . '}', $value, $text );
351 }
352 return $text;
353 }
354 #endregion
355
356 #region Image-Related Helpers
357 static function is_image( $file ) {
358 $mimeType = Meow_MWAI_Core::get_mime_type( $file );
359 if ( strpos( $mimeType, 'image' ) !== false ) {
360 return true;
361 }
362 return false;
363 }
364
365 static function get_image_resolution( $url ) {
366 if ( empty( $url ) ) {
367 return null;
368 }
369 $headers = get_headers( $url, 1 );
370 if ( strpos( $headers[0], '200' ) === false ) {
371 return null;
372 }
373 $image_info = getimagesize( $url );
374 if ( $image_info === false ) {
375 return null;
376 }
377 return [
378 'width' => $image_info[0],
379 'height' => $image_info[1]
380 ];
381 }
382
383 static function get_mime_type( $file ) {
384 $mimeType = null;
385
386 // Let's try to use mime_content_type if the function exists
387 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
388 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
389 $mimeType = mime_content_type( $file );
390 }
391
392 // Otherwise, let's check the file extension (which can actually also be an URL)
393 if ( !$mimeType ) {
394 $extension = pathinfo( $file, PATHINFO_EXTENSION );
395 $extension = strtolower( $extension );
396 $mimeTypes = [
397 'jpg' => 'image/jpeg',
398 'jpeg' => 'image/jpeg',
399 'png' => 'image/png',
400 'gif' => 'image/gif',
401 'webp' => 'image/webp',
402 'bmp' => 'image/bmp',
403 'tiff' => 'image/tiff',
404 'tif' => 'image/tiff',
405 'svg' => 'image/svg+xml',
406 'ico' => 'image/x-icon',
407 'pdf' => 'application/pdf',
408 ];
409 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
410 }
411
412 return $mimeType;
413 }
414
415 function download_image( $url ) {
416 $response = wp_safe_remote_get( $url, [ 'timeout' => 60 ] );
417 if ( is_wp_error( $response ) ) {
418 throw new Exception( $response->get_error_message() );
419 }
420 return wp_remote_retrieve_body( $response );
421 }
422
423 /**
424 * Add an image from a URL to the Media Library.
425 * @param string $url The URL of the image to be downloaded.
426 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
427 * @param string $title The title of the image.
428 * @param string $description The description of the image.
429 * @param string $caption The caption of the image.
430 * @param string $alt The alt text of the image.
431 * @return int The attachment ID of the image.
432 */
433 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null ) {
434 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
435 $url_filename = $path_parts['basename'];
436 $file_type = wp_check_filetype( $url_filename, null );
437 $allowed_types = get_allowed_mime_types();
438 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
439 throw new Exception( 'Invalid file type from URL.' );
440 }
441
442 // Initial extension from URL file name
443 $extension = $file_type['ext'];
444
445 if ( !empty( $filename ) ) {
446 $custom_file_type = wp_check_filetype( $filename, null );
447 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
448 throw new Exception( 'Invalid custom file type.' );
449 }
450 // Use the extension from the custom filename if valid
451 $extension = $custom_file_type['ext'];
452 }
453
454 $image_data = $this->download_image( $url );
455 if ( !$image_data ) {
456 throw new Exception( 'Could not download the image.' );
457 }
458 $upload_dir = wp_upload_dir();
459
460 // Filename handling including 'generated_' prefix scenario
461 if ( empty( $filename ) ) {
462 $filename = sanitize_file_name( $url_filename );
463 if ( empty( $extension ) ) { // This condition might now be redundant
464 $extension = $file_type['ext'];
465 }
466 // Filename length check and prepend if conditions met
467 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
468 $filename = $this->get_random_id( 16 ) . '.' . $extension;
469 }
470 if ( strpos( $filename, '.' ) === false ) {
471 $filename .= '.' . $extension;
472 }
473 }
474
475 // Directory and file path handling
476 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
477 $file = $upload_dir['path'] . '/' . $filename;
478 }
479 else {
480 $file = $upload_dir['basedir'] . '/' . $filename;
481 }
482
483 // Ensure file name uniqueness in the directory
484 $i = 1;
485 $parts = pathinfo( $file );
486 while ( file_exists( $file ) ) {
487 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
488 $i++;
489 }
490
491 // Writing the file to disk
492 file_put_contents( $file, $image_data );
493
494 // Attachment and metadata handling in WP
495 $attachment = [
496 'post_mime_type' => $file_type['type'],
497 'post_title' => $title ?? '',
498 'post_content' => $description ?? '',
499 'post_excerpt' => $caption ?? '',
500 'post_status' => 'inherit'
501 ];
502 $attachmentId = wp_insert_attachment( $attachment, $file );
503 require_once( ABSPATH . 'wp-admin/includes/image.php' );
504 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
505 wp_update_attachment_metadata( $attachmentId, $attachment_data );
506 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
507
508 // Attach the image to a post if needed
509 if ( $attachedPost ) {
510 wp_update_post( [ 'ID' => $attachmentId, 'post_parent' => $attachedPost ] );
511 }
512
513 return $attachmentId;
514 }
515 #endregion
516
517 #region Context-Related Helpers
518 function retrieve_context( $params, $query, $streamCallback = null ) {
519 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
520 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
521
522 $context = apply_filters( 'mwai_context_search', [], $query, [
523 'embeddingsEnvId' => $embeddingsEnvId,
524 'streamCallback' => $streamCallback
525 ]);
526
527 // Emit embeddings event if streaming and context was found
528 if ( $streamCallback && !empty( $context ) ) {
529 $count = 0;
530 if ( isset( $context['embeddings'] ) && is_array( $context['embeddings'] ) ) {
531 $count = count( $context['embeddings'] );
532 } else if ( isset( $context['content'] ) ) {
533 $count = 1;
534 }
535 if ( $count > 0 ) {
536 $event = Meow_MWAI_Event::embeddings( $count );
537 $streamCallback( $event );
538 }
539 }
540
541 if ( empty( $context ) ) {
542 return null;
543 }
544 else if ( !isset( $context['content'] ) ) {
545 Meow_MWAI_Logging::warn( "A context without content was returned." );
546 return null;
547 }
548 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
549 $context['length'] = strlen( $context['content'] );
550 return $context;
551 }
552 #endregion
553
554 #region Users/Sessions Helpers
555
556 function get_nonce( $force = false ) {
557 if ( !$force && !is_user_logged_in() ) {
558 return null;
559 }
560 if ( isset( $this->nonce ) ) {
561 return $this->nonce;
562 }
563 $this->nonce = wp_create_nonce( 'wp_rest' );
564 return $this->nonce;
565 }
566
567 // This is a bit hacky, but chatId needs to be retrieved or generated.
568 // Maybe we can clean this up later.
569 function fix_chat_id( $query, $params ) {
570 if ( isset( $query->chatId ) && $query->chatId !== 'N/A' ) {
571 return $query->chatId;
572 }
573 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : $query->session;
574 if ( $chatId === 'N/A' ) {
575 $chatId = $this->get_random_id( 8 );
576 }
577 $query->set_chat_id( $chatId );
578 return $chatId;
579 }
580
581 function get_session_id() {
582 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
583 return $_COOKIE['mwai_session_id'];
584 }
585 return "N/A";
586 }
587
588 // Get the UserID from the data, or from the current user
589 function get_user_id( $data = null ) {
590 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
591 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
592 $admin = get_users( [ 'role' => 'administrator' ] );
593 if ( !empty( $admin ) ) {
594 return $admin[0]->ID;
595 }
596 }
597 if ( isset( $data ) && isset( $data['userId'] ) ) {
598 return (int)$data['userId'];
599 }
600 if ( is_user_logged_in() ) {
601 $current_user = wp_get_current_user();
602 if ( $current_user->ID > 0 ) {
603 return $current_user->ID;
604 }
605 }
606 return null;
607 }
608
609 function get_admin_user() {
610 $admin = get_users( [ 'role' => 'administrator' ] );
611 if ( !empty( $admin ) ) {
612 return $admin[0];
613 }
614 return null;
615 }
616
617 function get_user_data() {
618 $user = wp_get_current_user();
619 if ( empty( $user ) || empty( $user->ID ) ) {
620 return null;
621 }
622 $placeholders = array(
623 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
624 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
625 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
626 $user->data->user_login : null,
627 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
628 $user->data->display_name : null,
629 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
630 );
631 return $placeholders;
632 }
633
634 function get_ip_address( $force = false ) {
635 $ip = '127.0.0.1';
636 $headers = [
637 'HTTP_TRUE_CLIENT_IP',
638 'HTTP_CF_CONNECTING_IP',
639 'HTTP_X_REAL_IP',
640 'HTTP_CLIENT_IP',
641 'HTTP_X_FORWARDED_FOR',
642 'HTTP_X_FORWARDED',
643 'HTTP_X_CLUSTER_CLIENT_IP',
644 'HTTP_FORWARDED_FOR',
645 'HTTP_FORWARDED',
646 'REMOTE_ADDR',
647 ];
648
649 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
650 $ip = ( string )$params[ 'ip' ];
651 }
652 else {
653 foreach ( $headers as $header ) {
654 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
655 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
656 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
657 break;
658 }
659 }
660 }
661
662 $ip = filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
663
664 // If privacy_first is enabled, we hash the IP address.
665 if ( !$force && $this->get_option( 'privacy_first' ) && !empty( $ip ) ) {
666 $hash = hash( 'sha256', $ip, true ); // binary output
667 $ip = substr( rtrim( strtr( base64_encode( $hash ), '+/', '-_'), '=' ), 0, 12 );
668 }
669
670 return $ip;
671 }
672
673 #endregion
674
675 #region Sanitization
676 function sanitize_sort( &$sort, $default_accessor = 'created', $default_order = 'DESC',
677 $allowed_columns = array( 'created', 'updated', 'name', 'id', 'time', 'units', 'price' )) {
678
679 // Ensure $sort is an array
680 if ( !is_array( $sort ) ) {
681 $sort = [ "accessor" => $default_accessor, "by" => $default_order ];
682 }
683 // Extract and sanitize the accessor
684 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
685 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
686 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
687 $sort_accessor = $default_accessor;
688 }
689 // Extract and sanitize the sort order
690 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
691 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
692 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
693 $sort_by = $default_order;
694 }
695 // Update the sort array with sanitized values
696 $sort['accessor'] = $sort_accessor;
697 $sort['by'] = $sort_by;
698 }
699 #endregion
700
701 #region Other Helpers
702 function safe_strlen( $string, $encoding = 'UTF-8' ) {
703 if ( function_exists( 'mb_strlen' ) ) {
704 return mb_strlen( $string, $encoding );
705 }
706 else {
707 // Fallback implementation for environments without mbstring extension
708 return preg_match_all( '/./u', $string, $matches );
709 }
710 }
711
712 public function check_rest_nonce( $request ) {
713 $nonce = $request->get_header( 'X-WP-Nonce' );
714 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
715 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
716 }
717
718 function get_random_id( $length = 8, $excludeIds = [] ) {
719 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
720 $charactersLength = strlen( $characters );
721 $randomId = '';
722 for ( $i = 0; $i < $length; $i++ ) {
723 $randomId .= $characters[ mt_rand( 0, $charactersLength - 1 ) ];
724 }
725 if ( in_array( $randomId, $excludeIds ) ) {
726 return $this->get_random_id( $length, $excludeIds );
727 }
728 return $randomId;
729 }
730
731 function is_url( $url ) {
732 return strpos( $url, 'http' ) === 0 ? true : false;
733 }
734
735 function get_post_types() {
736 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
737 $post_types = array();
738 $types = get_post_types( [], 'objects' );
739
740 // Let's get the Post Types that are enabled for Embeddings Sync
741 $embeddingsSettings = $this->get_option( 'embeddings' );
742 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
743
744 foreach ( $types as $type ) {
745 $forced = in_array( $type->name, $syncPostTypes );
746 // Should not be excluded.
747 if ( !$forced && in_array( $type->name, $excluded ) ) {
748 continue;
749 }
750 // Should be public.
751 if ( !$forced && !$type->public ) {
752 continue;
753 }
754 $post_types[] = array(
755 'name' => $type->labels->name,
756 'type' => $type->name,
757 );
758 }
759
760 // Let's get the Post Types that are enabled for Embeddings Sync
761 $embeddingsSettings = $this->get_option( 'embeddings' );
762 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
763
764 return $post_types;
765 }
766
767 function get_post( $post ) {
768 if ( is_numeric( $post ) ) {
769 $post = get_post( $post );
770 }
771 if ( is_object( $post ) ) {
772 $post = (array)$post;
773 }
774 if ( !is_array( $post ) ) {
775 return null;
776 }
777 $language = $this->get_post_language( $post['ID'] );
778 $content = $this->get_post_content( $post['ID'] );
779 $title = $post['post_title'];
780 $excerpt = $post['post_excerpt'];
781 $url = get_permalink( $post['ID'] );
782 $checksum = wp_hash( $content . $title . $url );
783 return [
784 'postId' => (int)$post['ID'],
785 'title' => $title,
786 'content' => $content,
787 'excerpt' => $excerpt,
788 'url' => $url,
789 'language' => $language ?? 'english',
790 'checksum' => $checksum,
791 ];
792 }
793 #endregion
794
795 #region Usage & Costs
796
797 // Quick and dirty token estimation
798 // Let's keep this synchronized with Helpers in JS
799 static function estimate_tokens( ...$args ): int {
800 $text = "";
801 foreach ( $args as $arg ) {
802 if ( is_array( $arg ) ) {
803 foreach ( $arg as $message ) {
804 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
805 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
806 }
807 }
808 else if ( is_string( $arg ) ) {
809 $text .= $arg;
810 }
811 }
812 $averageTokenLength = 4;
813 $words = preg_split( '/\s+/', trim( $text ) );
814 $tokenCount = 0;
815 foreach ( $words as $word ) {
816 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
817 }
818 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
819 }
820
821 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
822 if ( !is_numeric( $in_tokens ) ) {
823 throw new Exception( 'AI Engine: in_tokens must be a number.' );
824 }
825 if ( !is_numeric( $out_tokens ) ) {
826 $out_tokens = 0;
827 }
828 if ( !$model ) {
829 throw new Exception( 'AI Engine: model is required.' );
830 }
831 $usage = $this->get_option( 'ai_models_usage' );
832 $month = date( 'Y-m' );
833 if ( !isset( $usage[$month] ) ) {
834 $usage[$month] = array();
835 }
836 if ( !isset( $usage[$month][$model] ) ) {
837 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
838 }
839 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
840 $usage[$month][$model]['completion_tokens'] += $out_tokens;
841 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
842 $this->update_option( 'ai_models_usage', $usage );
843 $usageInfo = [
844 'prompt_tokens' => $in_tokens,
845 'completion_tokens' => $out_tokens,
846 'total_tokens' => $in_tokens + $out_tokens,
847 ];
848 if ( $returned_price !== null ) {
849 $usageInfo['price'] = $returned_price;
850 }
851 return $usageInfo;
852 }
853
854 public function record_audio_usage( $model, $seconds ) {
855 if ( !is_numeric( $seconds ) ) {
856 throw new Exception( 'AI Engine: seconds must be a number.' );
857 }
858 if ( !$model ) {
859 throw new Exception( 'AI Engine: model is required.' );
860 }
861 $usage = $this->get_option( 'ai_models_usage' );
862 $month = date( 'Y-m' );
863 if ( !isset( $usage[$month] ) ) {
864 $usage[$month] = array();
865 }
866 if ( !isset( $usage[$month][$model] ) ) {
867 $usage[$month][$model] = array( 'seconds' => 0 );
868 }
869 $usage[$month][$model]['seconds'] += $seconds;
870 $this->update_option( 'ai_models_usage', $usage );
871 return [ 'seconds' => $seconds ];
872 }
873
874 public function record_images_usage( $model, $resolution, $images ) {
875 if ( !$model || !$resolution || !$images ) {
876 throw new Exception( 'Missing parameters for record_image_usage.' );
877 }
878 $usage = $this->get_option( 'ai_models_usage' );
879 $month = date( 'Y-m' );
880 if ( !isset( $usage[$month] ) ) {
881 $usage[$month] = array();
882 }
883 if ( !isset( $usage[$month][$model] ) ) {
884 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
885 }
886 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
887 $usage[$month][$model]['resolution'][$resolution] = 0;
888 }
889 $usage[$month][$model]['resolution'][$resolution] += $images;
890 $usage[$month][$model]['images'] += $images;
891 $this->update_option( 'ai_models_usage', $usage );
892 return [ 'resolution' => $resolution, 'images' => $images ];
893 }
894
895 #endregion
896
897 #region Streaming
898 public function stream_push( $data, $query = null ) {
899 // Handle new Event objects
900 if ( is_object( $data ) && method_exists( $data, 'to_array' ) ) {
901 $data = $data->to_array();
902 }
903
904 $data = apply_filters( 'mwai_stream_push', $data, $query );
905 $out = "data: " . json_encode( $data );
906 echo $out;
907 echo "\n\n";
908 if ( ob_get_level() > 0 ) {
909 ob_end_flush();
910 }
911 flush();
912 }
913 #endregion
914
915 #region Options
916 function get_themes() {
917 $themes = get_option( $this->themes_option_name, [] );
918 $themes = empty( $themes ) ? [] : $themes;
919
920 $internalThemes = [
921 'chatgpt' => [
922 'type' => 'internal','name' => 'ChatGPT', 'themeId' => 'chatgpt',
923 'settings' => [], 'style' => ""
924 ],
925 'messages' => [
926 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
927 'settings' => [], 'style' => ""
928 ],
929 'timeless' => [
930 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
931 'settings' => [], 'style' => ""
932 ],
933 ];
934 $customThemes = [];
935 foreach ( $themes as $theme ) {
936 if ( isset( $internalThemes[$theme['themeId']] ) ) {
937 $internalThemes[$theme['themeId']] = $theme;
938 continue;
939 }
940 $customThemes[] = $theme;
941 }
942 return array_merge(array_values($internalThemes), $customThemes);
943 }
944
945 function update_themes( $themes ) {
946 update_option( $this->themes_option_name, $themes );
947 return $themes;
948 }
949
950 function get_chatbots() {
951 $chatbots = get_option( $this->chatbots_option_name, [] );
952 $hasChanges = false;
953 if ( empty( $chatbots ) ) {
954 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
955 }
956 $hasDefault = false;
957 foreach ( $chatbots as &$chatbot ) {
958 if ( $chatbot['botId'] === 'default' ) {
959 $hasDefault = true;
960 }
961 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
962 // Use default value if not set.
963 if ( !isset( $chatbot[$key] ) ) {
964 $chatbot[$key] = $value;
965 }
966 }
967
968 /*
969 This is the best section to rename fields.
970 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).
971 */
972
973 // if ( isset( $chatbot['context'] ) ) {
974 // $chatbot['instructions'] = $chatbot['context'];
975 // unset( $chatbot['context'] );
976 // $hasChanges = true;
977 // }
978 }
979 if ( !$hasDefault ) {
980 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
981 array_unshift( $chatbots, $defaultBot );
982 $hasChanges = true;
983 }
984 if ( $hasChanges ) {
985 update_option( $this->chatbots_option_name, $chatbots );
986 }
987 return $chatbots;
988 }
989
990 function get_chatbot( $botId ) {
991 $chatbots = $this->get_chatbots();
992 foreach ( $chatbots as $chatbot ) {
993 if ( $chatbot['botId'] === (string)$botId ) {
994 return $chatbot;
995 }
996 }
997 return null;
998 }
999
1000 function get_embeddings_env( $envId ) {
1001 $envs = $this->get_option( 'embeddings_envs' );
1002 foreach ( $envs as $env ) {
1003 if ( $env['id'] === $envId ) {
1004 return $env;
1005 }
1006 }
1007 return null;
1008 }
1009
1010 function get_ai_env( $envId ) {
1011 $envs = $this->get_option( 'ai_envs' );
1012 foreach ( $envs as $env ) {
1013 if ( $env['id'] === $envId ) {
1014 return $env;
1015 }
1016 }
1017 return null;
1018 }
1019
1020 function get_assistant( $envId, $assistantId ) {
1021 $env = $this->get_ai_env( $envId );
1022 if ( !$env ) {
1023 return null;
1024 }
1025 $assistants = $env['assistants'];
1026 foreach ( $assistants as $assistant ) {
1027 if ( $assistant['id'] === $assistantId ) {
1028 return $assistant;
1029 }
1030 }
1031 return null;
1032 }
1033
1034 function get_theme( $themeId ) {
1035 $themes = $this->get_themes();
1036 foreach ( $themes as $theme ) {
1037 if ( $theme['themeId'] === $themeId ) {
1038 return $theme;
1039 }
1040 }
1041 return null;
1042 }
1043
1044 function update_chatbots( $chatbots ) {
1045 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
1046 // TODO: I think some HTML fields are missing, guestName, maybe others.
1047 $htmlFields = [ 'instructions', 'textCompliance', 'aiName', 'userName', 'startSentence' ];
1048 $keepLineReturnsFields = [ 'instructions' ];
1049 $whiteSpacedFields = [ 'context' ];
1050 foreach ( $chatbots as &$chatbot ) {
1051 foreach ( $chatbot as $key => &$value ) {
1052 if ( in_array( $key, $deprecatedFields ) ) {
1053 unset( $chatbot[$key] );
1054 continue;
1055 }
1056 if ( in_array( $key, $htmlFields ) ) {
1057 $value = wp_kses_post( $value );
1058 }
1059 else if ( in_array( $key, $whiteSpacedFields ) ) {
1060 $value = sanitize_textarea_field( $value );
1061 }
1062 else if ( $key === 'functions' ) {
1063 $functions = [];
1064 foreach ( $value as $function ) {
1065 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
1066 $functions[] = [
1067 'id' => sanitize_text_field( $function['id'] ),
1068 'type' => sanitize_text_field( $function['type'] ),
1069 ];
1070 }
1071 }
1072 $value = $functions;
1073 }
1074 else if ( $key === 'mcpServers' ) {
1075 $mcpServers = [];
1076 foreach ( $value as $server ) {
1077 if ( isset( $server['id'] ) ) {
1078 $mcpServers[] = [
1079 'id' => sanitize_text_field( $server['id'] ),
1080 ];
1081 }
1082 }
1083 $value = $mcpServers;
1084 }
1085 else {
1086 if ( in_array( $key, $keepLineReturnsFields ) ) {
1087 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
1088 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
1089 }
1090 $value = sanitize_text_field( $value );
1091 if ( in_array( $key, $keepLineReturnsFields ) ) {
1092 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1093 }
1094 }
1095 }
1096 }
1097 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1098 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1099 $chatbots = get_option( $this->chatbots_option_name, [] );
1100 return $chatbots;
1101 }
1102 return $chatbots;
1103 }
1104
1105 function populate_dynamic_options( $options ) {
1106 static $populating = false;
1107
1108 // Prevent infinite recursion
1109 if ( $populating ) {
1110 return $options;
1111 }
1112
1113 $populating = true;
1114
1115 // Languages
1116 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1117
1118 // Consolidate the Engines and their Models
1119 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1120 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1121 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1122 foreach ( $options['ai_engines'] as &$engine ) {
1123 if ( $engine['type'] === 'openai' ) {
1124 $engine['models'] = apply_filters( 'mwai_openai_models',
1125 Meow_MWAI_Engines_OpenAI::get_models_static()
1126 );
1127 }
1128 else if ( $engine['type'] === 'anthropic' ) {
1129 $engine['models'] = apply_filters( 'mwai_anthropic_models',
1130 Meow_MWAI_Engines_Anthropic::get_models_static()
1131 );
1132 }
1133 else if ( $engine['type'] === 'perplexity' ) {
1134 $engine['models'] = apply_filters( 'mwai_perplexity_models',
1135 Meow_MWAI_Engines_Perplexity::get_models_static()
1136 );
1137 }
1138 else {
1139 $engine['models'] = [];
1140 foreach ( $options['ai_models'] as $model ) {
1141 if ( $model['type'] === $engine['type'] ) {
1142 $engine['models'][] = $model;
1143 }
1144 }
1145 }
1146 }
1147
1148 // Functions via Code Engine (or custom code)
1149 $json = [];
1150 $functions = apply_filters( 'mwai_functions_list', [] );
1151 foreach ( $functions as $function ) {
1152 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1153 }
1154 $options['functions'] = $json;
1155
1156 // Addons
1157 $options['addons'] = apply_filters( 'mwai_addons', [
1158 [
1159 "slug" => "mwai-notifications",
1160 "name" => "Notifications",
1161 "description" => "Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.",
1162 "install_url" => "https://meowapps.com/products/mwai-notifications/",
1163 "settings_url" => null,
1164 "stars" => 4,
1165 "enabled" => false
1166 ],
1167 [
1168 "slug" => "mwai-ollama",
1169 "name" => "Ollama",
1170 "description" => "Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.",
1171 "install_url" => "https://meowapps.com/products/mwai-ollama/",
1172 "settings_url" => null,
1173 "stars" => 3,
1174 "enabled" => false
1175 ],
1176 [
1177 "slug" => "mwai-deepseek",
1178 "name" => "DeepSeek",
1179 "description" => "Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.",
1180 "install_url" => "https://meowapps.com/products/deepseek/",
1181 "settings_url" => null,
1182 "stars" => 3,
1183 "enabled" => false
1184 ],
1185 [
1186 "slug" => "mwai-websearch",
1187 "name" => "Web Search",
1188 "description" => "Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.",
1189 "install_url" => "https://meowapps.com/products/mwai-websearch/",
1190 "settings_url" => null,
1191 "stars" => 5,
1192 "enabled" => false
1193 ],
1194 [
1195 "slug" => "mwai-better-links",
1196 "name" => "Better Links",
1197 "description" => "Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.",
1198 "install_url" => "https://meowapps.com/products/mwai-better-links/",
1199 "settings_url" => null,
1200 "stars" => 3,
1201 "enabled" => false
1202 ],
1203 [
1204 "slug" => "mwai-woo-basics",
1205 "name" => "Woo Basics",
1206 "description" => "Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.",
1207 "install_url" => "https://meowapps.com/products/mwai-woo-basics/",
1208 "settings_url" => null,
1209 "stars" => 2,
1210 "enabled" => false
1211 ],
1212 [
1213 "slug" => "mwai-quick-actions",
1214 "name" => "Quick Actions",
1215 "description" => "Enable dynamic quick actions at chat start or during events, helping users find what they need faster.",
1216 "install_url" => "https://meowapps.com/products/mwai-quick-actions/",
1217 "settings_url" => null,
1218 "stars" => 3,
1219 "enabled" => false
1220 ],
1221 [
1222 "slug" => "mwai-content-parser",
1223 "name" => "Content Parser",
1224 "description" => "Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.",
1225 "install_url" => "https://meowapps.com/products/mwai-content-parser/",
1226 "settings_url" => null,
1227 "stars" => 2,
1228 "enabled" => false
1229 ],
1230 [
1231 "slug" => "mwai-visitor-form",
1232 "name" => "Visitor Form",
1233 "description" => "Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.",
1234 "install_url" => "https://meowapps.com/products/mwai-visitor-form/",
1235 "settings_url" => null,
1236 "stars" => 2,
1237 "enabled" => false
1238 ],
1239 [
1240 "slug" => "mwai-dynamic-keys",
1241 "name" => "Dynamic Keys",
1242 "description" => "Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.",
1243 "install_url" => "https://meowapps.com/products/mwai-dynamic-keys/",
1244 "settings_url" => null,
1245 "stars" => 1,
1246 "enabled" => false
1247 ],
1248 ] );
1249
1250 $populating = false;
1251 return $options;
1252 }
1253
1254 function get_all_options( $force = false, $sanitize = false ) {
1255 if ( $force || is_null( $this->options ) ) {
1256 $options = get_option( $this->option_name, [] );
1257 $init_mode = empty( $options );
1258 foreach ( MWAI_OPTIONS as $key => $value ) {
1259 if ( !isset( $options[$key] ) ) {
1260 $options[$key] = $value;
1261 }
1262 }
1263 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1264 $options['default_limits'] = MWAI_LIMITS;
1265 if ( $sanitize || $init_mode ) {
1266 $options = $this->sanitize_options( $options );
1267 }
1268 $this->options = $options;
1269 }
1270 $options = $this->populate_dynamic_options( $this->options );
1271 return $options;
1272 }
1273
1274 // Sanitize options when we update the plugin or perform some updates
1275 // if we change the structure of the options.
1276 function sanitize_options( $options ) {
1277 $needs_update = false;
1278
1279 // Removing old options of options renaming should be done here, as it was done before.
1280 // Check version 2.6.8 for an example.
1281
1282 // Avoid the logs_path to be a PHP file.
1283 if ( isset( $options['logs_path'] ) ) {
1284 $logs_path = $options['logs_path'];
1285 if ( substr( $logs_path, -4 ) !== '.log' ) {
1286 $options['logs_path'] = '';
1287 $needs_update = true;
1288 }
1289 }
1290
1291 // The IDs for the embeddings environments are generated here.
1292 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1293 $embeddings_default_exists = false;
1294 if ( isset( $options['embeddings_envs'] ) ) {
1295 foreach ( $options['embeddings_envs'] as &$env ) {
1296 if ( !isset( $env['id'] ) ) {
1297 $env['id'] = $this->get_random_id();
1298 $needs_update = true;
1299 }
1300 if ( $env['id'] === $options['embeddings_default_env'] ) {
1301 $embeddings_default_exists = true;
1302 }
1303 }
1304 }
1305 if ( !$embeddings_default_exists ) {
1306 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1307 $needs_update = true;
1308 }
1309
1310 // The IDs for the AI environments are generated here.
1311 $allEnvIds = [];
1312 $ai_default_exists = false;
1313 if ( isset( $options['ai_envs'] ) ) {
1314 foreach ( $options['ai_envs'] as &$env ) {
1315 if ( !isset( $env['id'] ) ) {
1316 $env['id'] = $this->get_random_id();
1317 $needs_update = true;
1318 }
1319 if ( $env['id'] === $options['ai_default_env'] ) {
1320 $ai_default_exists = true;
1321 }
1322 $allEnvIds[] = $env['id'];
1323 }
1324 }
1325 if ( !$ai_default_exists ) {
1326 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1327 $needs_update = true;
1328 }
1329
1330 // The IDs for the MCP environments are generated here.
1331 if ( isset( $options['mcp_envs'] ) ) {
1332 foreach ( $options['mcp_envs'] as &$env ) {
1333 if ( !isset( $env['id'] ) ) {
1334 $env['id'] = $this->get_random_id();
1335 $needs_update = true;
1336 }
1337 }
1338 }
1339
1340 // All the models with an envId that does not exist anymore are removed.
1341 if ( isset( $options['ai_models'] ) ) {
1342 $options['ai_models'] = array_values( array_filter( $options['ai_models'],
1343 function( $model ) use ( $allEnvIds, &$needs_update ) {
1344 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1345 $needs_update = true;
1346 return false;
1347 }
1348 return true;
1349 }
1350 ) );
1351 }
1352
1353 if ( $needs_update ) {
1354 ksort( $options );
1355 update_option( $this->option_name, $options, false );
1356 }
1357
1358 return $options;
1359 }
1360
1361 function update_options( $options ) {
1362 if ( !update_option( $this->option_name, $options, false ) ) {
1363 return false;
1364 }
1365 $options = $this->get_all_options( true, true );
1366 return $options;
1367 }
1368
1369 function update_option( $option, $value ) {
1370 $options = $this->get_all_options( true );
1371 $options[$option] = $value;
1372 return $this->update_options( $options );
1373 }
1374
1375 function get_option( $option, $default = null ) {
1376 $options = $this->get_all_options();
1377 return $options[$option] ?? $default;
1378 }
1379
1380 function update_ai_env( $env_id, $option, $value ) {
1381 $options = $this->get_all_options( true );
1382 foreach ( $options['ai_envs'] as &$env ) {
1383 if ( $env['id'] === $env_id ) {
1384 $env[$option] = $value;
1385 break;
1386 }
1387 }
1388 return $this->update_options( $options );
1389 }
1390
1391 function get_engine_models( $engineType ) {
1392 $engines = $this->get_option( 'ai_engines' );
1393 foreach ( $engines as $engine ) {
1394 if ( $engine['type'] === $engineType ) {
1395 return isset( $engine['models'] ) ? $engine['models'] : [];
1396 }
1397 }
1398 return [];
1399 }
1400
1401 function reset_options() {
1402 delete_option( $this->themes_option_name );
1403 delete_option( $this->chatbots_option_name );
1404 delete_option( $this->option_name );
1405 return $this->get_all_options( true );
1406 }
1407 #endregion
1408 }
1409