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