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