PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.1.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.1.0
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
data 1 year ago engines 9 months ago exceptions 1 year ago modules 9 months ago query 10 months ago rest 10 months ago services 9 months ago admin.php 9 months ago api.php 10 months ago core.php 9 months ago discussion.php 1 year ago event.php 1 year ago init.php 1 year ago logging.php 1 year ago reply.php 10 months ago rest.php 9 months ago
core.php
1483 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 public $admin = null;
14 public $is_rest = false;
15 public $is_cli = false;
16 public $site_url = null;
17 public $files = null;
18 public $tasks = null;
19 public $magicWand = null;
20 private $options = null;
21 private $option_name = 'mwai_options';
22 private $themes_option_name = 'mwai_themes';
23 private $chatbots_option_name = 'mwai_chatbots';
24 private $nonce = null;
25
26 public $chatbot = null;
27 public $discussions = null;
28 public $search = null;
29
30 // Service instances for improved architecture
31 public $responseIdManager = null;
32 public $messageBuilder = null;
33 public $sessionService = null;
34 public $imageService = null;
35 public $usageStatsService = null;
36 public $modelEnvironmentService = null;
37
38 public function __construct() {
39 Meow_MWAI_Logging::init( 'mwai_options', 'AI Engine' );
40 $this->site_url = get_site_url();
41 $this->is_rest = MeowCommon_Helpers::is_rest();
42 $this->is_cli = defined( 'WP_CLI' );
43 $this->files = new Meow_MWAI_Modules_Files( $this );
44 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
45 $this->advisor = new Meow_MWAI_Modules_Advisor( $this );
46
47 // Load task examples in Dev Mode
48 if ( $this->get_option( 'dev_mode' ) ) {
49 new Meow_MWAI_Modules_Tasks_Examples( $this );
50 }
51
52 add_action( 'plugins_loaded', [ $this, 'init' ] );
53 add_action( 'wp_register_script', [ $this, 'register_scripts' ] );
54 add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
55 add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] );
56 }
57
58 #region Init & Scripts
59 public function init() {
60 global $mwai;
61 $this->chatbot = null;
62 $this->discussions = null;
63
64 // Initialize services here after autoloader is ready
65 $this->responseIdManager = new Meow_MWAI_Services_ResponseIdManager( $this );
66 $this->messageBuilder = new Meow_MWAI_Services_MessageBuilder( $this );
67 $this->sessionService = new Meow_MWAI_Services_Session( $this );
68 $this->imageService = new Meow_MWAI_Services_Image( $this );
69 $this->usageStatsService = new Meow_MWAI_Services_UsageStats( $this );
70 $this->modelEnvironmentService = new Meow_MWAI_Services_ModelEnvironment( $this );
71
72 // Start session early if needed for REST requests
73 if ( $this->is_rest && $this->sessionService->can_start_session() ) {
74 session_start();
75 }
76
77 new Meow_MWAI_Modules_Security( $this );
78
79 // REST API
80 if ( $this->is_rest ) {
81 new Meow_MWAI_Rest( $this );
82 }
83
84 // WP Admin
85 if ( is_admin() ) {
86 new Meow_MWAI_Admin( $this );
87 }
88
89 // GDPR Module
90 if ( $this->get_option( 'chatbot_gdpr_consent' ) ) {
91 new Meow_MWAI_Modules_GDPR( $this );
92 }
93
94 // Suggestions Module
95 if ( $this->get_option( 'module_suggestions' ) && ( is_admin() || $this->is_rest ) ) {
96 $this->magicWand = new Meow_MWAI_Modules_Wand( $this );
97 }
98
99
100 // Chatbots & Discussions
101 if ( $this->get_option( 'module_chatbots' ) ) {
102 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
103 // Only instantiate discussions if the feature is enabled
104 if ( $this->get_option( 'chatbot_discussions' ) ) {
105 $this->discussions = new Meow_MWAI_Modules_Discussions();
106 }
107 }
108
109 // Search
110 if ( $this->get_option( 'module_search' ) ) {
111 $this->search = new Meow_MWAI_Modules_Search( $this );
112 }
113
114 // Forms Manager (standalone Forms UI + shortcode renderer)
115 if ( $this->get_option( 'module_forms' ) ) {
116 new Meow_MWAI_Modules_Forms_Manager( $this );
117 }
118
119 // Advanced Core
120 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
121 new MeowPro_MWAI_Core( $this );
122 }
123
124 // Simple API
125 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions ?? null );
126
127 // MCP
128 if ( $this->get_option( 'module_mcp' ) ) {
129 new Meow_MWAI_Labs_MCP( $this );
130
131 // Core - Core WordPress MCP tools
132 if ( $this->get_option( 'mcp_core' ) ) {
133 new Meow_MWAI_Labs_MCP_Core( $this );
134 }
135
136 // Dynamic REST - WordPress REST API MCP tools
137 if ( $this->get_option( 'mcp_dynamic_rest' ) ) {
138 require_once MWAI_PATH . '/labs/mcp-rest.php';
139 new Meow_MWAI_Labs_MCP_Rest();
140 }
141
142 // Themes - Pro theme management MCP tools
143 if ( $this->get_option( 'mcp_themes' ) && class_exists( 'MeowPro_MWAI_MCP_Theme' ) ) {
144 new MeowPro_MWAI_MCP_Theme( $this );
145 }
146
147 // Plugins - Pro plugin management MCP tools
148 if ( $this->get_option( 'mcp_plugins' ) && class_exists( 'MeowPro_MWAI_MCP_Plugin' ) ) {
149 new MeowPro_MWAI_MCP_Plugin( $this );
150 }
151 }
152 }
153
154 public function register_scripts() {
155 // Register Highlight.js
156 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
157 // Register CSS for the themes
158 $themes = $this->get_themes();
159 foreach ( $themes as $theme ) {
160 if ( $theme['type'] === 'internal' ) {
161 $themeId = $theme['themeId'];
162 $filename = $themeId . '.css';
163 $physical_file = trailingslashit( MWAI_PATH ) . 'themes/' . $filename;
164 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
165 wp_register_style( 'mwai_chatbot_theme_' . $themeId, trailingslashit( MWAI_URL )
166 . 'themes/' . $filename, [], $cache_buster );
167 }
168 }
169 }
170
171 public function enqueue_theme( $themeId ) {
172 if ( empty( $themeId ) ) {
173 return;
174 }
175 wp_enqueue_style( "mwai_chatbot_theme_$themeId" );
176 }
177
178 public function enqueue_themes() {
179 $themes = $this->get_themes();
180 foreach ( $themes as $theme ) {
181 if ( $theme['type'] === 'internal' ) {
182 $this->enqueue_theme( $theme['themeId'] );
183 }
184 }
185 }
186
187 #endregion
188
189 #region Roles & Capabilities
190 public function can_start_session() {
191 return $this->sessionService->can_start_session();
192 }
193
194 public function can_access_settings() {
195 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
196 }
197
198 public function can_access_features() {
199 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
200 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
201 }
202
203 public function can_access_public_api( $feature, $extra ) {
204 $logged_in = is_user_logged_in();
205 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
206 }
207 #endregion
208
209 #region AI-Related Helpers
210 public function run_query( $query, $streamCallback = null, $markdown = false ) {
211
212 // Allow to modify the query before it is sent.
213 // Embedding and Feedback queries are not allowed to be modified.
214 if ( !( $query instanceof Meow_MWAI_Query_Embed ) && !( $query instanceof Meow_MWAI_Query_Feedback ) ) {
215 $query = apply_filters( 'mwai_ai_query', $query );
216 }
217
218 // Ensure the query is still valid after filtering
219 if ( !$query || !is_object( $query ) ) {
220 throw new Exception( __( 'Invalid query object after filtering. The mwai_ai_query filter must return a valid query object.', 'ai-engine' ) );
221 }
222
223 // Validate that embeddings queries have a non-empty message
224 if ( $query instanceof Meow_MWAI_Query_Embed && empty( $query->get_message() ) ) {
225 throw new Exception( __( 'Embeddings query cannot have an empty message. Please check that the conversation context is properly extracted.', 'ai-engine' ) );
226 }
227
228 // Let's check the default environment and model.
229 $this->validate_env_model( $query );
230
231 // Create the engine based on the query's environment
232 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
233
234 // Let's run the query.
235 $reply = $engine->run( $query, $streamCallback );
236
237 // Let's allow to modify the reply before it is sent.
238 if ( $markdown ) {
239 if ( $query instanceof Meow_MWAI_Query_Image || $query instanceof Meow_MWAI_Query_EditImage ) {
240 $reply->result = '';
241 foreach ( $reply->results as $result ) {
242 $reply->result .= "![Image]($result)\n";
243 }
244 }
245 }
246
247 return $reply;
248 }
249
250 public function validate_env_model( $query ) {
251 return $this->modelEnvironmentService->validate_env_model( $query );
252 }
253
254 #endregion
255
256 #region Text-Related Helpers
257
258 // Clean the text perfectly, resolve shortcodes, etc, etc.
259 public function clean_text( $rawText = '' ) {
260 $text = html_entity_decode( $rawText );
261 $text = wp_strip_all_tags( $text );
262 $text = preg_replace( '/[\r\n]+/', "\n", $text );
263 $text = preg_replace( '/\n+/', "\n", $text );
264 $text = preg_replace( '/\t+/', "\t", $text );
265 return $text . ' ';
266 }
267
268 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
269 public function clean_sentences( $text, $maxLength = null ) {
270 // Step 1: Identify URLs and replace them with a placeholder.
271 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
272 preg_match_all( $urlPattern, $text, $urls );
273 $urlPlaceholders = [];
274 foreach ( $urls[0] as $index => $url ) {
275 $placeholder = '{urlPlaceholder' . $index . '}';
276 $text = str_replace( $url, $placeholder, $text );
277 $urlPlaceholders[$placeholder] = $url;
278 }
279
280 $maxLength = (int) ( $maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ) );
281 $sentences = preg_split( '/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY );
282 $hashes = [];
283 $uniqueSentences = [];
284 $total = 0;
285
286 foreach ( $sentences as $sentence ) {
287 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
288 $hash = md5( $sentence );
289 if ( !in_array( $hash, $hashes ) ) {
290 $length = mb_strlen( $sentence, 'UTF-8' );
291 if ( $total + $length > $maxLength ) {
292 continue;
293 }
294 $hashes[] = $hash;
295 $uniqueSentences[] = $sentence;
296 $total += $length;
297 }
298 }
299
300 $freshText = implode( ' ', $uniqueSentences );
301
302 // Step 3: Restore URLs in the final text.
303 foreach ( $urlPlaceholders as $placeholder => $url ) {
304 $freshText = str_replace( $placeholder, $url, $freshText );
305 }
306
307 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
308 return $freshText;
309 }
310
311 public function get_post_content( $postId ) {
312 // Ensure we get fresh post data by clearing cache
313 clean_post_cache( $postId );
314 $post = get_post( $postId );
315 if ( !$post ) {
316 return false;
317 }
318 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
319 $pattern = '/\[mwai_.*?\]/';
320 $text = preg_replace( $pattern, '', $text );
321 if ( $this->get_option( 'resolve_shortcodes' ) ) {
322 $text = apply_filters( 'the_content', $text );
323 }
324 else {
325 $pattern = "/\[[^\]]+\]/";
326 $text = preg_replace( $pattern, '', $text );
327 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
328 $text = preg_replace( $pattern, '', $text );
329 }
330 $text = $this->clean_text( $text );
331 $text = $this->clean_sentences( $text );
332 $text = apply_filters( 'mwai_post_content', $text, $postId );
333 return $text;
334 }
335
336 public function markdown_to_html( $content ) {
337 $Parsedown = new Parsedown();
338 $content = $Parsedown->text( $content );
339 return $content;
340 }
341
342 public function get_post_language( $postId ) {
343 $locale = get_locale();
344 $code = strtolower( substr( $locale, 0, 2 ) );
345 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
346 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
347 if ( !empty( $lang ) ) {
348 $locale = $lang['locale'];
349 $humanLanguage = $lang['display_name'];
350 }
351 return strtolower( "$locale ($humanLanguage)" );
352 }
353
354 public function do_placeholders( $text ) {
355 $defaultPlaceholders = [];
356 $dataPlaceholders = $this->get_user_data();
357 if ( !empty( $dataPlaceholders ) ) {
358 $defaultPlaceholders = array_merge( $defaultPlaceholders, $dataPlaceholders );
359 }
360 $placeholders = apply_filters( 'mwai_placeholders', $defaultPlaceholders );
361 foreach ( $placeholders as $key => $value ) {
362 $text = str_replace( '{' . $key . '}', $value, $text );
363 }
364 return $text;
365 }
366 #endregion
367
368 #region Image-Related Helpers
369 public static function is_image( $file ) {
370 global $mwai_core;
371 if ( $mwai_core && $mwai_core->imageService ) {
372 return $mwai_core->imageService->is_image( $file );
373 }
374 // Fallback to original implementation if service not available
375 $mimeType = self::get_mime_type( $file );
376 if ( strpos( $mimeType, 'image' ) !== false ) {
377 return true;
378 }
379 return false;
380 }
381
382 public static function get_image_resolution( $url ) {
383 global $mwai_core;
384 if ( $mwai_core && $mwai_core->imageService ) {
385 return $mwai_core->imageService->get_image_resolution( $url );
386 }
387 // Fallback to original implementation if service not available
388 if ( empty( $url ) ) {
389 return null;
390 }
391 $headers = get_headers( $url, 1 );
392 if ( strpos( $headers[0], '200' ) === false ) {
393 return null;
394 }
395 $image_info = getimagesize( $url );
396 if ( $image_info === false ) {
397 return null;
398 }
399 return [
400 'width' => $image_info[0],
401 'height' => $image_info[1]
402 ];
403 }
404
405 public static function get_mime_type( $file ) {
406 global $mwai_core;
407 if ( $mwai_core && $mwai_core->imageService ) {
408 return $mwai_core->imageService->get_mime_type( $file );
409 }
410
411 // Fallback implementation - this should rarely be used as imageService is initialized early
412 Meow_MWAI_Logging::warn( 'get_mime_type called before imageService is available' );
413
414 // Basic extension-based detection only
415 $extension = pathinfo( $file, PATHINFO_EXTENSION );
416 $extension = strtolower( $extension );
417 $mimeTypes = [
418 'jpg' => 'image/jpeg',
419 'jpeg' => 'image/jpeg',
420 'png' => 'image/png',
421 'gif' => 'image/gif',
422 'webp' => 'image/webp',
423 'bmp' => 'image/bmp',
424 'tiff' => 'image/tiff',
425 'tif' => 'image/tiff',
426 'svg' => 'image/svg+xml',
427 'ico' => 'image/x-icon',
428 'pdf' => 'application/pdf',
429 ];
430 return isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
431 }
432
433 public function download_image( $url ) {
434 return $this->imageService->download_image( $url );
435 }
436
437 /**
438 * Add an image from a URL to the Media Library.
439 * @param string $url The URL of the image to be downloaded.
440 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
441 * @param string $title The title of the image.
442 * @param string $description The description of the image.
443 * @param string $caption The caption of the image.
444 * @param string $alt The alt text of the image.
445 * @return int The attachment ID of the image.
446 */
447 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null ) {
448 return $this->imageService->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, $attachedPost );
449 }
450 #endregion
451
452 #region Context-Related Helpers
453 public function retrieve_context( $params, $query, $streamCallback = null ) {
454 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
455 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
456
457 $context = apply_filters( 'mwai_context_search', [], $query, [
458 'embeddingsEnvId' => $embeddingsEnvId,
459 'streamCallback' => $streamCallback
460 ] );
461
462 // Emit embeddings event if streaming and context was found
463 if ( $streamCallback && !empty( $context ) ) {
464 $count = 0;
465 if ( isset( $context['embeddings'] ) && is_array( $context['embeddings'] ) ) {
466 $count = count( $context['embeddings'] );
467 }
468 else if ( isset( $context['content'] ) ) {
469 $count = 1;
470 }
471 if ( $count > 0 ) {
472 $event = Meow_MWAI_Event::embeddings( $count );
473 $streamCallback( $event );
474 }
475 }
476
477 if ( empty( $context ) ) {
478 return null;
479 }
480 else if ( !isset( $context['content'] ) ) {
481 Meow_MWAI_Logging::warn( 'A context without content was returned.' );
482 return null;
483 }
484 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
485 $context['length'] = strlen( $context['content'] );
486 return $context;
487 }
488 #endregion
489
490 #region Users/Sessions Helpers
491
492 public function get_nonce( $force = false ) {
493 return $this->sessionService->get_nonce( $force );
494 }
495
496 // This is a bit hacky, but chatId needs to be retrieved or generated.
497 // Maybe we can clean this up later.
498 public function fix_chat_id( $query, $params ) {
499 return $this->sessionService->fix_chat_id( $query, $params );
500 }
501
502 public function get_session_id() {
503 return $this->sessionService->get_session_id();
504 }
505
506 /**
507 * Get the Response ID Manager service
508 */
509 public function get_response_id_manager() {
510 return $this->responseIdManager;
511 }
512
513 /**
514 * Get the Message Builder service
515 */
516 public function get_message_builder() {
517 return $this->messageBuilder;
518 }
519
520 // Get the UserID from the data, or from the current user
521 public function get_user_id( $data = null ) {
522 return $this->sessionService->get_user_id( $data );
523 }
524
525 public function get_session_user_id() {
526 return $this->sessionService->get_session_user_id();
527 }
528
529 public function get_admin_user() {
530 return $this->sessionService->get_admin_user();
531 }
532
533 public function get_user_data() {
534 return $this->sessionService->get_user_data();
535 }
536
537 public function get_ip_address( $force = false ) {
538 return $this->sessionService->get_ip_address( $force );
539 }
540
541 #endregion
542
543 #region Sanitization
544 public function sanitize_sort(
545 &$sort,
546 $default_accessor = 'created',
547 $default_order = 'DESC',
548 $allowed_columns = [ 'created', 'updated', 'name', 'id', 'time', 'units', 'price' ]
549 ) {
550
551 // Ensure $sort is an array
552 if ( !is_array( $sort ) ) {
553 $sort = [ 'accessor' => $default_accessor, 'by' => $default_order ];
554 }
555 // Extract and sanitize the accessor
556 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
557 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
558 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
559 $sort_accessor = $default_accessor;
560 }
561 // Extract and sanitize the sort order
562 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
563 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
564 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
565 $sort_by = $default_order;
566 }
567 // Update the sort array with sanitized values
568 $sort['accessor'] = $sort_accessor;
569 $sort['by'] = $sort_by;
570 }
571 #endregion
572
573 #region Other Helpers
574 public function safe_strlen( $string, $encoding = 'UTF-8' ) {
575 if ( function_exists( 'mb_strlen' ) ) {
576 return mb_strlen( $string, $encoding );
577 }
578 else {
579 // Fallback implementation for environments without mbstring extension
580 return preg_match_all( '/./u', $string, $matches );
581 }
582 }
583
584 public function check_rest_nonce( $request ) {
585 // REST NONCE VERIFICATION:
586 // Validates nonce from X-WP-Nonce header using WordPress nonce system.
587 // Returns: false (invalid), 1 (0-12 hours old), or 2 (12-24 hours old)
588 // WordPress REST permission callbacks accept any truthy value as success.
589 // The filter allows custom authorization logic if needed.
590 $nonce = $request->get_header( 'X-WP-Nonce' );
591 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
592 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
593 }
594
595 public function get_random_id( $length = 8, $excludeIds = [] ) {
596 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
597 $charactersLength = strlen( $characters );
598 $randomId = '';
599 for ( $i = 0; $i < $length; $i++ ) {
600 $randomId .= $characters[ mt_rand( 0, $charactersLength - 1 ) ];
601 }
602 if ( in_array( $randomId, $excludeIds ) ) {
603 return $this->get_random_id( $length, $excludeIds );
604 }
605 return $randomId;
606 }
607
608 public function is_url( $url ) {
609 return strpos( $url, 'http' ) === 0 ? true : false;
610 }
611
612 public function get_post_types() {
613 $excluded = [ 'attachment', 'revision', 'nav_menu_item' ];
614 $post_types = [];
615 $types = get_post_types( [], 'objects' );
616
617 // Let's get the Post Types that are enabled for Embeddings Sync
618 $embeddingsSettings = $this->get_option( 'embeddings' );
619 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
620
621 foreach ( $types as $type ) {
622 $forced = in_array( $type->name, $syncPostTypes );
623 // Should not be excluded.
624 if ( !$forced && in_array( $type->name, $excluded ) ) {
625 continue;
626 }
627 // Should be public.
628 if ( !$forced && !$type->public ) {
629 continue;
630 }
631 $post_types[] = [
632 'name' => $type->labels->name,
633 'type' => $type->name,
634 ];
635 }
636
637 // Let's get the Post Types that are enabled for Embeddings Sync
638 $embeddingsSettings = $this->get_option( 'embeddings' );
639 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
640
641 return $post_types;
642 }
643
644 public function get_post( $post ) {
645 if ( is_numeric( $post ) ) {
646 // Force fresh retrieval to avoid cache issues
647 clean_post_cache( $post );
648 $post = get_post( $post );
649 }
650 if ( is_object( $post ) ) {
651 $post = (array) $post;
652 }
653 if ( !is_array( $post ) ) {
654 return null;
655 }
656 $language = $this->get_post_language( $post['ID'] );
657 $content = $this->get_post_content( $post['ID'] );
658 $title = $post['post_title'];
659 $excerpt = $post['post_excerpt'];
660 $url = get_permalink( $post['ID'] );
661 $checksum = wp_hash( $content . $title . $url );
662
663
664 return [
665 'postId' => (int) $post['ID'],
666 'title' => $title,
667 'content' => $content,
668 'excerpt' => $excerpt,
669 'url' => $url,
670 'language' => $language ?? 'english',
671 'checksum' => $checksum,
672 ];
673 }
674
675 /**
676 * Format a date/time string into a human-readable format
677 * @param string $date_string The date string to format
678 * @return string Formatted date (e.g., "Just now", "5m ago", "2h ago", "3d ago", "Jan 20th")
679 */
680 public function format_discussion_date( $date_string ) {
681 $date = strtotime( $date_string );
682 $now = time();
683 $diff = $now - $date;
684
685 // Less than a minute
686 if ( $diff < 60 ) {
687 return 'Just now';
688 }
689
690 // Less than an hour
691 if ( $diff < 3600 ) {
692 $minutes = floor( $diff / 60 );
693 return $minutes . 'm ago';
694 }
695
696 // Less than a day
697 if ( $diff < 86400 ) {
698 $hours = floor( $diff / 3600 );
699 return $hours . 'h ago';
700 }
701
702 // Less than a week
703 if ( $diff < 604800 ) {
704 $days = floor( $diff / 86400 );
705 return $days . 'd ago';
706 }
707
708 // Format as date
709 $is_current_year = date( 'Y', $date ) === date( 'Y', $now );
710 if ( $is_current_year ) {
711 return date( 'M jS', $date );
712 } else {
713 return date( 'M jS, Y', $date );
714 }
715 }
716 #endregion
717
718 #region Usage & Costs
719
720 // Quick and dirty token estimation
721 // Let's keep this synchronized with Helpers in JS
722 public static function estimate_tokens( ...$args ): int {
723 global $mwai_core;
724 if ( $mwai_core && $mwai_core->usageStatsService ) {
725 return $mwai_core->usageStatsService->estimate_tokens( ...$args );
726 }
727 // Fallback to original implementation if service not available
728 $text = '';
729 foreach ( $args as $arg ) {
730 if ( is_array( $arg ) ) {
731 foreach ( $arg as $message ) {
732 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : '';
733 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : '';
734 }
735 }
736 else if ( is_string( $arg ) ) {
737 $text .= $arg;
738 }
739 }
740 $averageTokenLength = 4;
741 $words = preg_split( '/\s+/', trim( $text ) );
742 $tokenCount = 0;
743 foreach ( $words as $word ) {
744 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
745 }
746 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
747 }
748
749 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
750 return $this->usageStatsService->record_tokens_usage( $model, $in_tokens, $out_tokens, $returned_price );
751 }
752
753 public function record_audio_usage( $model, $seconds ) {
754 return $this->usageStatsService->record_audio_usage( $model, $seconds );
755 }
756
757 public function record_images_usage( $model, $resolution, $images ) {
758 return $this->usageStatsService->record_images_usage( $model, $resolution, $images );
759 }
760
761 #endregion
762
763 #region Streaming
764 public function stream_push( $data, $query = null ) {
765 try {
766 // Handle new Event objects
767 if ( is_object( $data ) && method_exists( $data, 'to_array' ) ) {
768 $data = $data->to_array();
769 }
770
771 $data = apply_filters( 'mwai_stream_push', $data, $query );
772 $out = 'data: ' . json_encode( $data );
773 echo $out;
774 echo "\n\n";
775 if ( ob_get_level() > 0 ) {
776 ob_end_flush();
777 }
778 flush();
779 }
780 catch ( Exception $e ) {
781 // Send error as proper SSE error event
782 $errorMessage = 'Oops! Something went wrong on the server. Please try again, and if you are the site developer, check the PHP Error Logs for details.';
783 error_log( '[AI Engine Stream Error] ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
784
785 $errorData = [
786 'type' => 'error',
787 'data' => $errorMessage
788 ];
789 $out = 'data: ' . json_encode( $errorData );
790 echo $out;
791 echo "\n\n";
792 if ( ob_get_level() > 0 ) {
793 ob_end_flush();
794 }
795 flush();
796
797 // Stop execution after sending error
798 die();
799 }
800 }
801 #endregion
802
803 #region Options
804 public function get_themes() {
805 $themes = get_option( $this->themes_option_name, [] );
806 $themes = empty( $themes ) ? [] : $themes;
807
808 $internalThemes = [
809 'chatgpt' => [
810 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
811 'settings' => [], 'style' => ''
812 ],
813 'messages' => [
814 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
815 'settings' => [], 'style' => ''
816 ],
817 'timeless' => [
818 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
819 'settings' => [], 'style' => ''
820 ],
821 ];
822 $customThemes = [];
823 foreach ( $themes as $theme ) {
824 if ( isset( $internalThemes[$theme['themeId']] ) ) {
825 $internalThemes[$theme['themeId']] = $theme;
826 continue;
827 }
828 $customThemes[] = $theme;
829 }
830 return array_merge( array_values( $internalThemes ), $customThemes );
831 }
832
833 public function update_themes( $themes ) {
834 update_option( $this->themes_option_name, $themes );
835 return $themes;
836 }
837
838 public function get_chatbots() {
839 $chatbots = get_option( $this->chatbots_option_name, [] );
840 $hasChanges = false;
841 if ( empty( $chatbots ) ) {
842 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
843 }
844 $hasDefault = false;
845 foreach ( $chatbots as &$chatbot ) {
846 if ( $chatbot['botId'] === 'default' ) {
847 $hasDefault = true;
848 }
849 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
850 // Use default value if not set.
851 if ( !isset( $chatbot[$key] ) ) {
852 $chatbot[$key] = $value;
853 }
854 }
855
856 /*
857 This is the best section to rename fields.
858 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).
859 */
860
861 // if ( isset( $chatbot['context'] ) ) {
862 // $chatbot['instructions'] = $chatbot['context'];
863 // unset( $chatbot['context'] );
864 // $hasChanges = true;
865 // }
866 }
867 if ( !$hasDefault ) {
868 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
869 array_unshift( $chatbots, $defaultBot );
870 $hasChanges = true;
871 }
872 if ( $hasChanges ) {
873 update_option( $this->chatbots_option_name, $chatbots );
874 }
875 return $chatbots;
876 }
877
878 public function get_chatbot( $botId ) {
879 $chatbots = $this->get_chatbots();
880 foreach ( $chatbots as $chatbot ) {
881 if ( $chatbot['botId'] === (string) $botId ) {
882 return $chatbot;
883 }
884 }
885 return null;
886 }
887
888 public function get_embeddings_env( $envId ) {
889 return $this->modelEnvironmentService->get_embeddings_env( $envId );
890 }
891
892 public function get_ai_env( $envId ) {
893 return $this->modelEnvironmentService->get_ai_env( $envId );
894 }
895
896 public function get_assistant( $envId, $assistantId ) {
897 return $this->modelEnvironmentService->get_assistant( $envId, $assistantId );
898 }
899
900 public function get_theme( $themeId ) {
901 $themes = $this->get_themes();
902 foreach ( $themes as $theme ) {
903 if ( $theme['themeId'] === $themeId ) {
904 // Append custom CSS to theme data for frontend rendering (check for non-empty trimmed string)
905 if ( isset( $theme['settings']['customCSS'] ) && trim( $theme['settings']['customCSS'] ) !== '' ) {
906 $customCSS = $theme['settings']['customCSS'];
907
908 // Add theme class prefix to all CSS rules for proper scoping
909 $themeClass = '.mwai-' . $themeId . '-theme';
910 $lines = explode( "\n", $customCSS );
911 $processedCSS = '';
912 $inRule = false;
913
914 foreach ( $lines as $line ) {
915 $trimmedLine = trim( $line );
916
917 // Skip empty lines and comments
918 if ( empty( $trimmedLine ) || strpos( $trimmedLine, '/*' ) === 0 ) {
919 $processedCSS .= $line . "\n";
920 continue;
921 }
922
923 // If line contains a selector (has { but not })
924 if ( strpos( $line, '{' ) !== false && strpos( $line, '}' ) === false ) {
925 // Extract selector and the rest
926 $parts = explode( '{', $line, 2 );
927 $selector = trim( $parts[0] );
928
929 // Add theme class prefix if not already present
930 if ( strpos( $selector, $themeClass ) !== 0 ) {
931 // Handle multiple selectors separated by comma
932 $selectors = explode( ',', $selector );
933 $prefixedSelectors = array_map( function( $sel ) use ( $themeClass ) {
934 $sel = trim( $sel );
935 // Don't prefix if it's a keyframe or similar
936 if ( strpos( $sel, '@' ) === 0 || strpos( $sel, 'from' ) === 0 || strpos( $sel, 'to' ) === 0 || preg_match( '/^\d+%/', $sel ) ) {
937 return $sel;
938 }
939 return $themeClass . ' ' . $sel;
940 }, $selectors );
941 $selector = implode( ', ', $prefixedSelectors );
942 }
943
944 $processedCSS .= $selector . ' {' . ( isset( $parts[1] ) ? $parts[1] : '' ) . "\n";
945 $inRule = true;
946 } else {
947 $processedCSS .= $line . "\n";
948 }
949 }
950
951 $customCSS = $processedCSS;
952
953 // For custom themes (type: 'css'), append to style property
954 if ( $theme['type'] === 'css' ) {
955 $theme['style'] = ( $theme['style'] ?? '' ) . "\n\n/* Custom CSS */\n" . $customCSS;
956 }
957 // For internal themes, add customCSS as a separate property
958 else {
959 $theme['customCSS'] = $customCSS;
960 }
961 }
962 return $theme;
963 }
964 }
965 return null;
966 }
967
968 public function update_chatbots( $chatbots ) {
969 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
970 // TODO: I think some HTML fields are missing, guestName, maybe others.
971 $htmlFields = [ 'instructions', 'textCompliance', 'aiName', 'userName', 'startSentence' ];
972 $keepLineReturnsFields = [ 'instructions' ];
973 $whiteSpacedFields = [ 'context' ];
974 // Boolean fields that need proper conversion
975 $booleanFields = [ 'window', 'copyButton', 'fullscreen', 'localMemory', 'iconBubble', 'centerOpen',
976 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch', 'contentAware', 'aiAvatar', 'userAvatar', 'guestAvatar' ];
977 foreach ( $chatbots as &$chatbot ) {
978 foreach ( $chatbot as $key => &$value ) {
979 if ( in_array( $key, $deprecatedFields ) ) {
980 unset( $chatbot[$key] );
981 continue;
982 }
983 if ( in_array( $key, $htmlFields ) ) {
984 $value = wp_kses_post( $value );
985 }
986 else if ( in_array( $key, $whiteSpacedFields ) ) {
987 $value = sanitize_textarea_field( $value );
988 }
989 else if ( in_array( $key, $booleanFields ) ) {
990 // Convert various representations to boolean
991 if ( is_bool( $value ) ) {
992 // Already boolean, keep as is
993 } else if ( $value === 1 || $value === '1' || $value === true || $value === 'true' || $value === 'yes' ) {
994 // These are true values
995 $value = true;
996 } else if ( $value === 0 || $value === '0' || $value === false || $value === 'false' || $value === 'no' || $value === '' || $value === null ) {
997 // These are false values
998 $value = false;
999 } else {
1000 // Default to checking if not empty
1001 $value = !empty( $value );
1002 }
1003 }
1004 else if ( $key === 'functions' ) {
1005 $functions = [];
1006 foreach ( $value as $function ) {
1007 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
1008 $functions[] = [
1009 'id' => sanitize_text_field( $function['id'] ),
1010 'type' => sanitize_text_field( $function['type'] ),
1011 ];
1012 }
1013 }
1014 $value = $functions;
1015 }
1016 else if ( $key === 'mcpServers' ) {
1017 $mcpServers = [];
1018 foreach ( $value as $server ) {
1019 if ( isset( $server['id'] ) ) {
1020 $mcpServers[] = [
1021 'id' => sanitize_text_field( $server['id'] ),
1022 ];
1023 }
1024 }
1025 $value = $mcpServers;
1026 }
1027 else if ( $key === 'tools' ) {
1028 // Sanitize tools array (web_search, image_generation, thinking, etc)
1029 $tools = [];
1030 if ( is_array( $value ) ) {
1031 foreach ( $value as $tool ) {
1032 $sanitized_tool = sanitize_text_field( $tool );
1033 if ( in_array( $sanitized_tool, ['web_search', 'image_generation', 'thinking', 'code_interpreter'] ) ) {
1034 $tools[] = $sanitized_tool;
1035 }
1036 }
1037 }
1038 $value = $tools;
1039 }
1040 else if ( $key === 'crossSite' ) {
1041 // Handle crossSite object
1042 $crossSite = [
1043 'enabled' => isset( $value['enabled'] ) ? (bool) $value['enabled'] : false,
1044 'allowedDomains' => []
1045 ];
1046 if ( isset( $value['allowedDomains'] ) && is_array( $value['allowedDomains'] ) ) {
1047 foreach ( $value['allowedDomains'] as $domain ) {
1048 $sanitized_domain = sanitize_text_field( $domain );
1049 if ( !empty( $sanitized_domain ) ) {
1050 $crossSite['allowedDomains'][] = $sanitized_domain;
1051 }
1052 }
1053 }
1054 $value = $crossSite;
1055 }
1056 else {
1057 if ( in_array( $key, $keepLineReturnsFields ) ) {
1058 $value = preg_replace( '/\r\n/', '[==LINE_RETURN==]', $value );
1059 $value = preg_replace( '/\n/', '[==LINE_RETURN==]', $value );
1060 }
1061 $value = sanitize_text_field( $value );
1062 if ( in_array( $key, $keepLineReturnsFields ) ) {
1063 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1064 }
1065 }
1066 }
1067 }
1068 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1069 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1070 $chatbots = get_option( $this->chatbots_option_name, [] );
1071 return $chatbots;
1072 }
1073 return $chatbots;
1074 }
1075
1076 public function populate_dynamic_options( $options ) {
1077 static $populating = false;
1078
1079 // Prevent infinite recursion
1080 if ( $populating ) {
1081 return $options;
1082 }
1083
1084 $populating = true;
1085
1086 // Languages - use custom languages as the complete list
1087 $custom_languages = isset( $options['custom_languages'] ) && !empty( $options['custom_languages'] )
1088 ? $options['custom_languages']
1089 : [];
1090
1091 // If no custom languages defined, fall back to defaults
1092 if ( empty( $custom_languages ) ) {
1093 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1094 } else {
1095 // Process custom languages
1096 $processed_languages = [];
1097 foreach ( $custom_languages as $custom_lang ) {
1098 // Support formats like "Russian (ru)" or just "Russian"
1099 $custom_lang = trim( $custom_lang );
1100 if ( !empty( $custom_lang ) ) {
1101 // Check if language code is provided in parentheses
1102 if ( preg_match( '/^(.+)\s*\(([a-z]{2,3})\)$/i', $custom_lang, $matches ) ) {
1103 $lang_name = trim( $matches[1] );
1104 $lang_code = strtolower( trim( $matches[2] ) );
1105 $processed_languages[$lang_code] = $lang_name;
1106 } else {
1107 // No code provided, add as-is
1108 $processed_languages[] = $custom_lang;
1109 }
1110 }
1111 }
1112
1113 $options['languages'] = apply_filters( 'mwai_languages', $processed_languages );
1114 }
1115
1116 // Consolidate the Engines and their Models
1117 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1118 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1119 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1120 foreach ( $options['ai_engines'] as &$engine ) {
1121 if ( $engine['type'] === 'openai' ) {
1122 $engine['models'] = apply_filters(
1123 'mwai_openai_models',
1124 Meow_MWAI_Engines_OpenAI::get_models_static()
1125 );
1126 }
1127 else if ( $engine['type'] === 'anthropic' ) {
1128 $engine['models'] = apply_filters(
1129 'mwai_anthropic_models',
1130 Meow_MWAI_Engines_Anthropic::get_models_static()
1131 );
1132 }
1133 else if ( $engine['type'] === 'perplexity' ) {
1134 $engine['models'] = apply_filters(
1135 'mwai_perplexity_models',
1136 Meow_MWAI_Engines_Perplexity::get_models_static()
1137 );
1138 }
1139 else {
1140 $engine['models'] = [];
1141 foreach ( $options['ai_models'] as $model ) {
1142 if ( $model['type'] === $engine['type'] ) {
1143 $engine['models'][] = $model;
1144 }
1145 }
1146 }
1147 }
1148
1149 // Functions via Code Engine (or custom code)
1150 $json = [];
1151 $functions = apply_filters( 'mwai_functions_list', [] );
1152 foreach ( $functions as $function ) {
1153 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1154 }
1155 $options['functions'] = $json;
1156
1157 // Addons
1158 $options['addons'] = apply_filters( 'mwai_addons', [
1159 [
1160 'slug' => 'mwai-notifications',
1161 'name' => 'Notifications',
1162 'description' => 'Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.',
1163 'install_url' => 'https://meowapps.com/products/mwai-notifications/',
1164 'settings_url' => null,
1165 'stars' => 4,
1166 'enabled' => false
1167 ],
1168 [
1169 'slug' => 'mwai-ollama',
1170 'name' => 'Ollama',
1171 'description' => 'Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.',
1172 'install_url' => 'https://meowapps.com/products/mwai-ollama/',
1173 'settings_url' => null,
1174 'stars' => 3,
1175 'enabled' => false
1176 ],
1177 [
1178 'slug' => 'mwai-deepseek',
1179 'name' => 'DeepSeek',
1180 'description' => 'Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.',
1181 'install_url' => 'https://meowapps.com/products/deepseek/',
1182 'settings_url' => null,
1183 'stars' => 3,
1184 'enabled' => false
1185 ],
1186 [
1187 'slug' => 'mwai-websearch',
1188 'name' => 'Web Search',
1189 'description' => 'Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.',
1190 'install_url' => 'https://meowapps.com/products/mwai-websearch/',
1191 'settings_url' => null,
1192 'stars' => 5,
1193 'enabled' => false
1194 ],
1195 [
1196 'slug' => 'mwai-better-links',
1197 'name' => 'Better Links',
1198 'description' => 'Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.',
1199 'install_url' => 'https://meowapps.com/products/mwai-better-links/',
1200 'settings_url' => null,
1201 'stars' => 3,
1202 'enabled' => false
1203 ],
1204 [
1205 'slug' => 'mwai-woo-basics',
1206 'name' => 'Woo Basics',
1207 'description' => 'Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.',
1208 'install_url' => 'https://meowapps.com/products/mwai-woo-basics/',
1209 'settings_url' => null,
1210 'stars' => 2,
1211 'enabled' => false
1212 ],
1213 [
1214 'slug' => 'mwai-quick-actions',
1215 'name' => 'Quick Actions',
1216 'description' => 'Enable dynamic quick actions at chat start or during events, helping users find what they need faster.',
1217 'install_url' => 'https://meowapps.com/products/mwai-quick-actions/',
1218 'settings_url' => null,
1219 'stars' => 3,
1220 'enabled' => false
1221 ],
1222 [
1223 'slug' => 'mwai-content-parser',
1224 'name' => 'Content Parser',
1225 'description' => 'Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.',
1226 'install_url' => 'https://meowapps.com/products/mwai-content-parser/',
1227 'settings_url' => null,
1228 'stars' => 2,
1229 'enabled' => false
1230 ],
1231 [
1232 'slug' => 'mwai-visitor-form',
1233 'name' => 'Visitor Form',
1234 'description' => 'Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.',
1235 'install_url' => 'https://meowapps.com/products/mwai-visitor-form/',
1236 'settings_url' => null,
1237 'stars' => 2,
1238 'enabled' => false
1239 ],
1240 [
1241 'slug' => 'mwai-dynamic-keys',
1242 'name' => 'Dynamic Keys',
1243 'description' => 'Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.',
1244 'install_url' => 'https://meowapps.com/products/mwai-dynamic-keys/',
1245 'settings_url' => null,
1246 'stars' => 1,
1247 'enabled' => false
1248 ],
1249 ] );
1250
1251 // Populate usage data from ai_usage to ai_models_usage for the frontend
1252 $ai_usage = $this->get_option( 'ai_usage', [] );
1253 $options['ai_models_usage'] = $ai_usage;
1254
1255 // Also include daily usage data
1256 $ai_usage_daily = $this->get_option( 'ai_usage_daily', [] );
1257 $options['ai_models_usage_daily'] = $ai_usage_daily;
1258
1259 $populating = false;
1260 return $options;
1261 }
1262
1263 public function get_all_options( $force = false, $sanitize = false ) {
1264 if ( $force || is_null( $this->options ) ) {
1265 $options = get_option( $this->option_name, [] );
1266 $init_mode = empty( $options );
1267 foreach ( MWAI_OPTIONS as $key => $value ) {
1268 if ( !isset( $options[$key] ) ) {
1269 $options[$key] = $value;
1270 }
1271 }
1272 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1273 $options['default_limits'] = MWAI_LIMITS;
1274
1275 // Force sanitization if custom_languages is not set (migration)
1276 $needs_language_migration = !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] );
1277
1278 if ( $sanitize || $init_mode || $needs_language_migration ) {
1279 $options = $this->sanitize_options( $options );
1280 }
1281 $this->options = $options;
1282 }
1283 $options = $this->populate_dynamic_options( $this->options );
1284 return $options;
1285 }
1286
1287 // Sanitize options when we update the plugin or perform some updates
1288 // if we change the structure of the options.
1289 public function sanitize_options( $options ) {
1290 $needs_update = false;
1291
1292 // Removing old options of options renaming should be done here, as it was done before.
1293 // Check version 2.6.8 for an example.
1294
1295 // Avoid the logs_path to be a PHP file.
1296 if ( isset( $options['logs_path'] ) ) {
1297 $logs_path = $options['logs_path'];
1298 if ( substr( $logs_path, -4 ) !== '.log' ) {
1299 $options['logs_path'] = '';
1300 $needs_update = true;
1301 }
1302 }
1303
1304 // The IDs for the embeddings environments are generated here.
1305 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1306 $embeddings_default_exists = false;
1307 if ( isset( $options['embeddings_envs'] ) ) {
1308 foreach ( $options['embeddings_envs'] as &$env ) {
1309 if ( !isset( $env['id'] ) ) {
1310 $env['id'] = $this->get_random_id();
1311 $needs_update = true;
1312 }
1313 if ( $env['id'] === $options['embeddings_default_env'] ) {
1314 $embeddings_default_exists = true;
1315 }
1316 }
1317 }
1318 if ( !$embeddings_default_exists ) {
1319 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1320 $needs_update = true;
1321 }
1322
1323 // The IDs for the AI environments are generated here.
1324 $allEnvIds = [];
1325 $ai_default_exists = false;
1326 if ( isset( $options['ai_envs'] ) ) {
1327 foreach ( $options['ai_envs'] as &$env ) {
1328 if ( !isset( $env['id'] ) ) {
1329 $env['id'] = $this->get_random_id();
1330 $needs_update = true;
1331 }
1332 if ( $env['id'] === $options['ai_default_env'] ) {
1333 $ai_default_exists = true;
1334 }
1335 $allEnvIds[] = $env['id'];
1336 }
1337 }
1338 if ( !$ai_default_exists ) {
1339 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1340 $needs_update = true;
1341 }
1342
1343 // The IDs for the MCP environments are generated here.
1344 if ( isset( $options['mcp_envs'] ) ) {
1345 foreach ( $options['mcp_envs'] as &$env ) {
1346 if ( !isset( $env['id'] ) ) {
1347 $env['id'] = $this->get_random_id();
1348 $needs_update = true;
1349 }
1350 }
1351 }
1352
1353 // All the models with an envId that does not exist anymore are removed.
1354 if ( isset( $options['ai_models'] ) ) {
1355 $options['ai_models'] = array_values( array_filter(
1356 $options['ai_models'],
1357 function ( $model ) use ( $allEnvIds, &$needs_update ) {
1358 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1359 $needs_update = true;
1360 return false;
1361 }
1362 return true;
1363 }
1364 ) );
1365 }
1366
1367 // Migration: Populate custom_languages if empty for existing installations
1368 if ( !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] ) ) {
1369 $options['custom_languages'] = [
1370 'English (en)',
1371 'German (de)',
1372 'French (fr)',
1373 'Spanish (es)',
1374 'Italian (it)',
1375 'Chinese (zh)',
1376 'Japanese (ja)',
1377 'Portuguese (pt)'
1378 ];
1379 $needs_update = true;
1380 }
1381
1382 if ( $needs_update ) {
1383 ksort( $options );
1384 update_option( $this->option_name, $options, false );
1385 }
1386
1387 return $options;
1388 }
1389
1390 public function update_options( $options ) {
1391 if ( !update_option( $this->option_name, $options, false ) ) {
1392 return false;
1393 }
1394 $options = $this->get_all_options( true, true );
1395 return $options;
1396 }
1397
1398 public function update_option( $option, $value ) {
1399 $options = $this->get_all_options( true );
1400 $options[$option] = $value;
1401 return $this->update_options( $options );
1402 }
1403
1404 public function get_option( $option, $default = null ) {
1405 $options = $this->get_all_options();
1406 return $options[$option] ?? $default;
1407 }
1408
1409 public function update_ai_env( $env_id, $option, $value ) {
1410 $options = $this->get_all_options( true );
1411 foreach ( $options['ai_envs'] as &$env ) {
1412 if ( $env['id'] === $env_id ) {
1413 $env[$option] = $value;
1414 break;
1415 }
1416 }
1417 return $this->update_options( $options );
1418 }
1419
1420 public function get_engine_models( $engineType ) {
1421 // This method is called by engines with just a string type
1422 // We need to get the models differently
1423 $options = $this->get_all_options();
1424 $engines = $options['ai_envs'];
1425 $models = [];
1426
1427 // Find all models for this engine type
1428 foreach ( $engines as $engine ) {
1429 if ( $engine['type'] === $engineType ) {
1430 if ( isset( $engine['models'] ) ) {
1431 foreach ( $engine['models'] as $model ) {
1432 $models[] = $model;
1433 }
1434 }
1435 }
1436 }
1437
1438 // Also check custom models
1439 if ( isset( $options['ai_models'] ) ) {
1440 foreach ( $options['ai_models'] as $model ) {
1441 if ( $model['type'] === $engineType ) {
1442 $models[] = $model;
1443 }
1444 }
1445 }
1446
1447 return $models;
1448 }
1449
1450 public function reset_options() {
1451 delete_option( $this->themes_option_name );
1452 delete_option( $this->chatbots_option_name );
1453 delete_option( $this->option_name );
1454 return $this->get_all_options( true );
1455 }
1456 #endregion
1457
1458 #region Cron Tracking
1459 public function track_cron_start( $hook ) {
1460 // Set running transient (expires in 5 minutes as a safety measure)
1461 set_transient( 'mwai_cron_running_' . $hook, true, 300 );
1462 }
1463
1464 public function track_cron_end( $hook, $status = 'success', $error_message = '' ) {
1465 // Remove running transient
1466 delete_transient( 'mwai_cron_running_' . $hook );
1467
1468 // Get existing data
1469 $cron_data = get_transient( 'mwai_cron_last_run' ) ?: [];
1470
1471 // Update this cron's data - use time() for consistency
1472 $cron_data[$hook] = [
1473 'time' => time(),
1474 'status' => $status,
1475 'error' => $error_message
1476 ];
1477
1478 // Store for 7 days
1479 set_transient( 'mwai_cron_last_run', $cron_data, 7 * DAY_IN_SECONDS );
1480 }
1481 #endregion
1482 }
1483