PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.2
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / core.php
ai-engine / classes Last commit date
data 1 year ago engines 11 months ago exceptions 1 year ago modules 11 months ago query 1 year ago rest 1 year 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
1333 lines
1 <?php
2
3 require_once( MWAI_PATH . '/vendor/autoload.php' );
4 require_once( MWAI_PATH . '/constants/init.php' );
5
6 define( 'MWAI_IMG_WAND', MWAI_URL . '/images/wand.png' );
7 define( 'MWAI_IMG_WAND_HTML', "<img style='height: 22px; margin-bottom: -5px; margin-right: 8px;'
8 src='" . MWAI_IMG_WAND . "' alt='AI Wand' />" );
9 define( 'MWAI_IMG_WAND_HTML_XS', "<img style='height: 16px; margin-bottom: -2px;'
10 src='" . MWAI_IMG_WAND . "' alt='AI Wand' />" );
11
12 class Meow_MWAI_Core {
13 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_admin_user() {
519 return $this->sessionService->get_admin_user();
520 }
521
522 public function get_user_data() {
523 return $this->sessionService->get_user_data();
524 }
525
526 public function get_ip_address( $force = false ) {
527 return $this->sessionService->get_ip_address( $force );
528 }
529
530 #endregion
531
532 #region Sanitization
533 public function sanitize_sort(
534 &$sort,
535 $default_accessor = 'created',
536 $default_order = 'DESC',
537 $allowed_columns = [ 'created', 'updated', 'name', 'id', 'time', 'units', 'price' ]
538 ) {
539
540 // Ensure $sort is an array
541 if ( !is_array( $sort ) ) {
542 $sort = [ 'accessor' => $default_accessor, 'by' => $default_order ];
543 }
544 // Extract and sanitize the accessor
545 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
546 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
547 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
548 $sort_accessor = $default_accessor;
549 }
550 // Extract and sanitize the sort order
551 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
552 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
553 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
554 $sort_by = $default_order;
555 }
556 // Update the sort array with sanitized values
557 $sort['accessor'] = $sort_accessor;
558 $sort['by'] = $sort_by;
559 }
560 #endregion
561
562 #region Other Helpers
563 public function safe_strlen( $string, $encoding = 'UTF-8' ) {
564 if ( function_exists( 'mb_strlen' ) ) {
565 return mb_strlen( $string, $encoding );
566 }
567 else {
568 // Fallback implementation for environments without mbstring extension
569 return preg_match_all( '/./u', $string, $matches );
570 }
571 }
572
573 public function check_rest_nonce( $request ) {
574 // REST NONCE VERIFICATION:
575 // Validates nonce from X-WP-Nonce header using WordPress nonce system.
576 // Returns: false (invalid), 1 (0-12 hours old), or 2 (12-24 hours old)
577 // WordPress REST permission callbacks accept any truthy value as success.
578 // The filter allows custom authorization logic if needed.
579 $nonce = $request->get_header( 'X-WP-Nonce' );
580 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
581 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
582 }
583
584 public function get_random_id( $length = 8, $excludeIds = [] ) {
585 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
586 $charactersLength = strlen( $characters );
587 $randomId = '';
588 for ( $i = 0; $i < $length; $i++ ) {
589 $randomId .= $characters[ mt_rand( 0, $charactersLength - 1 ) ];
590 }
591 if ( in_array( $randomId, $excludeIds ) ) {
592 return $this->get_random_id( $length, $excludeIds );
593 }
594 return $randomId;
595 }
596
597 public function is_url( $url ) {
598 return strpos( $url, 'http' ) === 0 ? true : false;
599 }
600
601 public function get_post_types() {
602 $excluded = [ 'attachment', 'revision', 'nav_menu_item' ];
603 $post_types = [];
604 $types = get_post_types( [], 'objects' );
605
606 // Let's get the Post Types that are enabled for Embeddings Sync
607 $embeddingsSettings = $this->get_option( 'embeddings' );
608 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
609
610 foreach ( $types as $type ) {
611 $forced = in_array( $type->name, $syncPostTypes );
612 // Should not be excluded.
613 if ( !$forced && in_array( $type->name, $excluded ) ) {
614 continue;
615 }
616 // Should be public.
617 if ( !$forced && !$type->public ) {
618 continue;
619 }
620 $post_types[] = [
621 'name' => $type->labels->name,
622 'type' => $type->name,
623 ];
624 }
625
626 // Let's get the Post Types that are enabled for Embeddings Sync
627 $embeddingsSettings = $this->get_option( 'embeddings' );
628 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
629
630 return $post_types;
631 }
632
633 public function get_post( $post ) {
634 if ( is_numeric( $post ) ) {
635 // Force fresh retrieval to avoid cache issues
636 clean_post_cache( $post );
637 $post = get_post( $post );
638 }
639 if ( is_object( $post ) ) {
640 $post = (array) $post;
641 }
642 if ( !is_array( $post ) ) {
643 return null;
644 }
645 $language = $this->get_post_language( $post['ID'] );
646 $content = $this->get_post_content( $post['ID'] );
647 $title = $post['post_title'];
648 $excerpt = $post['post_excerpt'];
649 $url = get_permalink( $post['ID'] );
650 $checksum = wp_hash( $content . $title . $url );
651
652
653 return [
654 'postId' => (int) $post['ID'],
655 'title' => $title,
656 'content' => $content,
657 'excerpt' => $excerpt,
658 'url' => $url,
659 'language' => $language ?? 'english',
660 'checksum' => $checksum,
661 ];
662 }
663
664 /**
665 * Format a date/time string into a human-readable format
666 * @param string $date_string The date string to format
667 * @return string Formatted date (e.g., "Just now", "5m ago", "2h ago", "3d ago", "Jan 20th")
668 */
669 public function format_discussion_date( $date_string ) {
670 $date = strtotime( $date_string );
671 $now = time();
672 $diff = $now - $date;
673
674 // Less than a minute
675 if ( $diff < 60 ) {
676 return 'Just now';
677 }
678
679 // Less than an hour
680 if ( $diff < 3600 ) {
681 $minutes = floor( $diff / 60 );
682 return $minutes . 'm ago';
683 }
684
685 // Less than a day
686 if ( $diff < 86400 ) {
687 $hours = floor( $diff / 3600 );
688 return $hours . 'h ago';
689 }
690
691 // Less than a week
692 if ( $diff < 604800 ) {
693 $days = floor( $diff / 86400 );
694 return $days . 'd ago';
695 }
696
697 // Format as date
698 $is_current_year = date( 'Y', $date ) === date( 'Y', $now );
699 if ( $is_current_year ) {
700 return date( 'M jS', $date );
701 } else {
702 return date( 'M jS, Y', $date );
703 }
704 }
705 #endregion
706
707 #region Usage & Costs
708
709 // Quick and dirty token estimation
710 // Let's keep this synchronized with Helpers in JS
711 public static function estimate_tokens( ...$args ): int {
712 global $mwai_core;
713 if ( $mwai_core && $mwai_core->usageStatsService ) {
714 return $mwai_core->usageStatsService->estimate_tokens( ...$args );
715 }
716 // Fallback to original implementation if service not available
717 $text = '';
718 foreach ( $args as $arg ) {
719 if ( is_array( $arg ) ) {
720 foreach ( $arg as $message ) {
721 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : '';
722 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : '';
723 }
724 }
725 else if ( is_string( $arg ) ) {
726 $text .= $arg;
727 }
728 }
729 $averageTokenLength = 4;
730 $words = preg_split( '/\s+/', trim( $text ) );
731 $tokenCount = 0;
732 foreach ( $words as $word ) {
733 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
734 }
735 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
736 }
737
738 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
739 return $this->usageStatsService->record_tokens_usage( $model, $in_tokens, $out_tokens, $returned_price );
740 }
741
742 public function record_audio_usage( $model, $seconds ) {
743 return $this->usageStatsService->record_audio_usage( $model, $seconds );
744 }
745
746 public function record_images_usage( $model, $resolution, $images ) {
747 return $this->usageStatsService->record_images_usage( $model, $resolution, $images );
748 }
749
750 #endregion
751
752 #region Streaming
753 public function stream_push( $data, $query = null ) {
754 // Handle new Event objects
755 if ( is_object( $data ) && method_exists( $data, 'to_array' ) ) {
756 $data = $data->to_array();
757 }
758
759 $data = apply_filters( 'mwai_stream_push', $data, $query );
760 $out = 'data: ' . json_encode( $data );
761 echo $out;
762 echo "\n\n";
763 if ( ob_get_level() > 0 ) {
764 ob_end_flush();
765 }
766 flush();
767 }
768 #endregion
769
770 #region Options
771 public function get_themes() {
772 $themes = get_option( $this->themes_option_name, [] );
773 $themes = empty( $themes ) ? [] : $themes;
774
775 $internalThemes = [
776 'chatgpt' => [
777 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
778 'settings' => [], 'style' => ''
779 ],
780 'messages' => [
781 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
782 'settings' => [], 'style' => ''
783 ],
784 'timeless' => [
785 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
786 'settings' => [], 'style' => ''
787 ],
788 ];
789 $customThemes = [];
790 foreach ( $themes as $theme ) {
791 if ( isset( $internalThemes[$theme['themeId']] ) ) {
792 $internalThemes[$theme['themeId']] = $theme;
793 continue;
794 }
795 $customThemes[] = $theme;
796 }
797 return array_merge( array_values( $internalThemes ), $customThemes );
798 }
799
800 public function update_themes( $themes ) {
801 update_option( $this->themes_option_name, $themes );
802 return $themes;
803 }
804
805 public function get_chatbots() {
806 $chatbots = get_option( $this->chatbots_option_name, [] );
807 $hasChanges = false;
808 if ( empty( $chatbots ) ) {
809 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
810 }
811 $hasDefault = false;
812 foreach ( $chatbots as &$chatbot ) {
813 if ( $chatbot['botId'] === 'default' ) {
814 $hasDefault = true;
815 }
816 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
817 // Use default value if not set.
818 if ( !isset( $chatbot[$key] ) ) {
819 $chatbot[$key] = $value;
820 }
821 }
822
823 /*
824 This is the best section to rename fields.
825 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).
826 */
827
828 // if ( isset( $chatbot['context'] ) ) {
829 // $chatbot['instructions'] = $chatbot['context'];
830 // unset( $chatbot['context'] );
831 // $hasChanges = true;
832 // }
833 }
834 if ( !$hasDefault ) {
835 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
836 array_unshift( $chatbots, $defaultBot );
837 $hasChanges = true;
838 }
839 if ( $hasChanges ) {
840 update_option( $this->chatbots_option_name, $chatbots );
841 }
842 return $chatbots;
843 }
844
845 public function get_chatbot( $botId ) {
846 $chatbots = $this->get_chatbots();
847 foreach ( $chatbots as $chatbot ) {
848 if ( $chatbot['botId'] === (string) $botId ) {
849 return $chatbot;
850 }
851 }
852 return null;
853 }
854
855 public function get_embeddings_env( $envId ) {
856 return $this->modelEnvironmentService->get_embeddings_env( $envId );
857 }
858
859 public function get_ai_env( $envId ) {
860 return $this->modelEnvironmentService->get_ai_env( $envId );
861 }
862
863 public function get_assistant( $envId, $assistantId ) {
864 return $this->modelEnvironmentService->get_assistant( $envId, $assistantId );
865 }
866
867 public function get_theme( $themeId ) {
868 $themes = $this->get_themes();
869 foreach ( $themes as $theme ) {
870 if ( $theme['themeId'] === $themeId ) {
871 return $theme;
872 }
873 }
874 return null;
875 }
876
877 public function update_chatbots( $chatbots ) {
878 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
879 // TODO: I think some HTML fields are missing, guestName, maybe others.
880 $htmlFields = [ 'instructions', 'textCompliance', 'aiName', 'userName', 'startSentence' ];
881 $keepLineReturnsFields = [ 'instructions' ];
882 $whiteSpacedFields = [ 'context' ];
883 foreach ( $chatbots as &$chatbot ) {
884 foreach ( $chatbot as $key => &$value ) {
885 if ( in_array( $key, $deprecatedFields ) ) {
886 unset( $chatbot[$key] );
887 continue;
888 }
889 if ( in_array( $key, $htmlFields ) ) {
890 $value = wp_kses_post( $value );
891 }
892 else if ( in_array( $key, $whiteSpacedFields ) ) {
893 $value = sanitize_textarea_field( $value );
894 }
895 else if ( $key === 'functions' ) {
896 $functions = [];
897 foreach ( $value as $function ) {
898 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
899 $functions[] = [
900 'id' => sanitize_text_field( $function['id'] ),
901 'type' => sanitize_text_field( $function['type'] ),
902 ];
903 }
904 }
905 $value = $functions;
906 }
907 else if ( $key === 'mcpServers' ) {
908 $mcpServers = [];
909 foreach ( $value as $server ) {
910 if ( isset( $server['id'] ) ) {
911 $mcpServers[] = [
912 'id' => sanitize_text_field( $server['id'] ),
913 ];
914 }
915 }
916 $value = $mcpServers;
917 }
918 else if ( $key === 'tools' ) {
919 // Sanitize tools array (web_search, image_generation, thinking, etc)
920 $tools = [];
921 if ( is_array( $value ) ) {
922 foreach ( $value as $tool ) {
923 $sanitized_tool = sanitize_text_field( $tool );
924 if ( in_array( $sanitized_tool, ['web_search', 'image_generation', 'thinking'] ) ) {
925 $tools[] = $sanitized_tool;
926 }
927 }
928 }
929 $value = $tools;
930 }
931 else {
932 if ( in_array( $key, $keepLineReturnsFields ) ) {
933 $value = preg_replace( '/\r\n/', '[==LINE_RETURN==]', $value );
934 $value = preg_replace( '/\n/', '[==LINE_RETURN==]', $value );
935 }
936 $value = sanitize_text_field( $value );
937 if ( in_array( $key, $keepLineReturnsFields ) ) {
938 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
939 }
940 }
941 }
942 }
943 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
944 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
945 $chatbots = get_option( $this->chatbots_option_name, [] );
946 return $chatbots;
947 }
948 return $chatbots;
949 }
950
951 public function populate_dynamic_options( $options ) {
952 static $populating = false;
953
954 // Prevent infinite recursion
955 if ( $populating ) {
956 return $options;
957 }
958
959 $populating = true;
960
961 // Languages - use custom languages as the complete list
962 $custom_languages = isset( $options['custom_languages'] ) && !empty( $options['custom_languages'] )
963 ? $options['custom_languages']
964 : [];
965
966 // If no custom languages defined, fall back to defaults
967 if ( empty( $custom_languages ) ) {
968 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
969 } else {
970 // Process custom languages
971 $processed_languages = [];
972 foreach ( $custom_languages as $custom_lang ) {
973 // Support formats like "Russian (ru)" or just "Russian"
974 $custom_lang = trim( $custom_lang );
975 if ( !empty( $custom_lang ) ) {
976 // Check if language code is provided in parentheses
977 if ( preg_match( '/^(.+)\s*\(([a-z]{2,3})\)$/i', $custom_lang, $matches ) ) {
978 $lang_name = trim( $matches[1] );
979 $lang_code = strtolower( trim( $matches[2] ) );
980 $processed_languages[$lang_code] = $lang_name;
981 } else {
982 // No code provided, add as-is
983 $processed_languages[] = $custom_lang;
984 }
985 }
986 }
987
988 $options['languages'] = apply_filters( 'mwai_languages', $processed_languages );
989 }
990
991 // Consolidate the Engines and their Models
992 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
993 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
994 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
995 foreach ( $options['ai_engines'] as &$engine ) {
996 if ( $engine['type'] === 'openai' ) {
997 $engine['models'] = apply_filters(
998 'mwai_openai_models',
999 Meow_MWAI_Engines_OpenAI::get_models_static()
1000 );
1001 }
1002 else if ( $engine['type'] === 'anthropic' ) {
1003 $engine['models'] = apply_filters(
1004 'mwai_anthropic_models',
1005 Meow_MWAI_Engines_Anthropic::get_models_static()
1006 );
1007 }
1008 else if ( $engine['type'] === 'perplexity' ) {
1009 $engine['models'] = apply_filters(
1010 'mwai_perplexity_models',
1011 Meow_MWAI_Engines_Perplexity::get_models_static()
1012 );
1013 }
1014 else {
1015 $engine['models'] = [];
1016 foreach ( $options['ai_models'] as $model ) {
1017 if ( $model['type'] === $engine['type'] ) {
1018 $engine['models'][] = $model;
1019 }
1020 }
1021 }
1022 }
1023
1024 // Functions via Code Engine (or custom code)
1025 $json = [];
1026 $functions = apply_filters( 'mwai_functions_list', [] );
1027 foreach ( $functions as $function ) {
1028 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1029 }
1030 $options['functions'] = $json;
1031
1032 // Addons
1033 $options['addons'] = apply_filters( 'mwai_addons', [
1034 [
1035 'slug' => 'mwai-notifications',
1036 'name' => 'Notifications',
1037 'description' => 'Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.',
1038 'install_url' => 'https://meowapps.com/products/mwai-notifications/',
1039 'settings_url' => null,
1040 'stars' => 4,
1041 'enabled' => false
1042 ],
1043 [
1044 'slug' => 'mwai-ollama',
1045 'name' => 'Ollama',
1046 'description' => 'Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.',
1047 'install_url' => 'https://meowapps.com/products/mwai-ollama/',
1048 'settings_url' => null,
1049 'stars' => 3,
1050 'enabled' => false
1051 ],
1052 [
1053 'slug' => 'mwai-deepseek',
1054 'name' => 'DeepSeek',
1055 'description' => 'Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.',
1056 'install_url' => 'https://meowapps.com/products/deepseek/',
1057 'settings_url' => null,
1058 'stars' => 3,
1059 'enabled' => false
1060 ],
1061 [
1062 'slug' => 'mwai-websearch',
1063 'name' => 'Web Search',
1064 'description' => 'Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.',
1065 'install_url' => 'https://meowapps.com/products/mwai-websearch/',
1066 'settings_url' => null,
1067 'stars' => 5,
1068 'enabled' => false
1069 ],
1070 [
1071 'slug' => 'mwai-better-links',
1072 'name' => 'Better Links',
1073 'description' => 'Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.',
1074 'install_url' => 'https://meowapps.com/products/mwai-better-links/',
1075 'settings_url' => null,
1076 'stars' => 3,
1077 'enabled' => false
1078 ],
1079 [
1080 'slug' => 'mwai-woo-basics',
1081 'name' => 'Woo Basics',
1082 'description' => 'Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.',
1083 'install_url' => 'https://meowapps.com/products/mwai-woo-basics/',
1084 'settings_url' => null,
1085 'stars' => 2,
1086 'enabled' => false
1087 ],
1088 [
1089 'slug' => 'mwai-quick-actions',
1090 'name' => 'Quick Actions',
1091 'description' => 'Enable dynamic quick actions at chat start or during events, helping users find what they need faster.',
1092 'install_url' => 'https://meowapps.com/products/mwai-quick-actions/',
1093 'settings_url' => null,
1094 'stars' => 3,
1095 'enabled' => false
1096 ],
1097 [
1098 'slug' => 'mwai-content-parser',
1099 'name' => 'Content Parser',
1100 'description' => 'Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.',
1101 'install_url' => 'https://meowapps.com/products/mwai-content-parser/',
1102 'settings_url' => null,
1103 'stars' => 2,
1104 'enabled' => false
1105 ],
1106 [
1107 'slug' => 'mwai-visitor-form',
1108 'name' => 'Visitor Form',
1109 'description' => 'Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.',
1110 'install_url' => 'https://meowapps.com/products/mwai-visitor-form/',
1111 'settings_url' => null,
1112 'stars' => 2,
1113 'enabled' => false
1114 ],
1115 [
1116 'slug' => 'mwai-dynamic-keys',
1117 'name' => 'Dynamic Keys',
1118 'description' => 'Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.',
1119 'install_url' => 'https://meowapps.com/products/mwai-dynamic-keys/',
1120 'settings_url' => null,
1121 'stars' => 1,
1122 'enabled' => false
1123 ],
1124 ] );
1125
1126 // Populate usage data from ai_usage to ai_models_usage for the frontend
1127 $ai_usage = $this->get_option( 'ai_usage', [] );
1128 $options['ai_models_usage'] = $ai_usage;
1129
1130 // Also include daily usage data
1131 $ai_usage_daily = $this->get_option( 'ai_usage_daily', [] );
1132 $options['ai_models_usage_daily'] = $ai_usage_daily;
1133
1134 $populating = false;
1135 return $options;
1136 }
1137
1138 public function get_all_options( $force = false, $sanitize = false ) {
1139 if ( $force || is_null( $this->options ) ) {
1140 $options = get_option( $this->option_name, [] );
1141 $init_mode = empty( $options );
1142 foreach ( MWAI_OPTIONS as $key => $value ) {
1143 if ( !isset( $options[$key] ) ) {
1144 $options[$key] = $value;
1145 }
1146 }
1147 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1148 $options['default_limits'] = MWAI_LIMITS;
1149
1150 // Force sanitization if custom_languages is not set (migration)
1151 $needs_language_migration = !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] );
1152
1153 if ( $sanitize || $init_mode || $needs_language_migration ) {
1154 $options = $this->sanitize_options( $options );
1155 }
1156 $this->options = $options;
1157 }
1158 $options = $this->populate_dynamic_options( $this->options );
1159 return $options;
1160 }
1161
1162 // Sanitize options when we update the plugin or perform some updates
1163 // if we change the structure of the options.
1164 public function sanitize_options( $options ) {
1165 $needs_update = false;
1166
1167 // Removing old options of options renaming should be done here, as it was done before.
1168 // Check version 2.6.8 for an example.
1169
1170 // Avoid the logs_path to be a PHP file.
1171 if ( isset( $options['logs_path'] ) ) {
1172 $logs_path = $options['logs_path'];
1173 if ( substr( $logs_path, -4 ) !== '.log' ) {
1174 $options['logs_path'] = '';
1175 $needs_update = true;
1176 }
1177 }
1178
1179 // The IDs for the embeddings environments are generated here.
1180 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1181 $embeddings_default_exists = false;
1182 if ( isset( $options['embeddings_envs'] ) ) {
1183 foreach ( $options['embeddings_envs'] as &$env ) {
1184 if ( !isset( $env['id'] ) ) {
1185 $env['id'] = $this->get_random_id();
1186 $needs_update = true;
1187 }
1188 if ( $env['id'] === $options['embeddings_default_env'] ) {
1189 $embeddings_default_exists = true;
1190 }
1191 }
1192 }
1193 if ( !$embeddings_default_exists ) {
1194 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1195 $needs_update = true;
1196 }
1197
1198 // The IDs for the AI environments are generated here.
1199 $allEnvIds = [];
1200 $ai_default_exists = false;
1201 if ( isset( $options['ai_envs'] ) ) {
1202 foreach ( $options['ai_envs'] as &$env ) {
1203 if ( !isset( $env['id'] ) ) {
1204 $env['id'] = $this->get_random_id();
1205 $needs_update = true;
1206 }
1207 if ( $env['id'] === $options['ai_default_env'] ) {
1208 $ai_default_exists = true;
1209 }
1210 $allEnvIds[] = $env['id'];
1211 }
1212 }
1213 if ( !$ai_default_exists ) {
1214 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1215 $needs_update = true;
1216 }
1217
1218 // The IDs for the MCP environments are generated here.
1219 if ( isset( $options['mcp_envs'] ) ) {
1220 foreach ( $options['mcp_envs'] as &$env ) {
1221 if ( !isset( $env['id'] ) ) {
1222 $env['id'] = $this->get_random_id();
1223 $needs_update = true;
1224 }
1225 }
1226 }
1227
1228 // All the models with an envId that does not exist anymore are removed.
1229 if ( isset( $options['ai_models'] ) ) {
1230 $options['ai_models'] = array_values( array_filter(
1231 $options['ai_models'],
1232 function ( $model ) use ( $allEnvIds, &$needs_update ) {
1233 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1234 $needs_update = true;
1235 return false;
1236 }
1237 return true;
1238 }
1239 ) );
1240 }
1241
1242 // Migration: Populate custom_languages if empty for existing installations
1243 if ( !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] ) ) {
1244 $options['custom_languages'] = [
1245 'English (en)',
1246 'German (de)',
1247 'French (fr)',
1248 'Spanish (es)',
1249 'Italian (it)',
1250 'Chinese (zh)',
1251 'Japanese (ja)',
1252 'Portuguese (pt)'
1253 ];
1254 $needs_update = true;
1255 }
1256
1257 if ( $needs_update ) {
1258 ksort( $options );
1259 update_option( $this->option_name, $options, false );
1260 }
1261
1262 return $options;
1263 }
1264
1265 public function update_options( $options ) {
1266 if ( !update_option( $this->option_name, $options, false ) ) {
1267 return false;
1268 }
1269 $options = $this->get_all_options( true, true );
1270 return $options;
1271 }
1272
1273 public function update_option( $option, $value ) {
1274 $options = $this->get_all_options( true );
1275 $options[$option] = $value;
1276 return $this->update_options( $options );
1277 }
1278
1279 public function get_option( $option, $default = null ) {
1280 $options = $this->get_all_options();
1281 return $options[$option] ?? $default;
1282 }
1283
1284 public function update_ai_env( $env_id, $option, $value ) {
1285 $options = $this->get_all_options( true );
1286 foreach ( $options['ai_envs'] as &$env ) {
1287 if ( $env['id'] === $env_id ) {
1288 $env[$option] = $value;
1289 break;
1290 }
1291 }
1292 return $this->update_options( $options );
1293 }
1294
1295 public function get_engine_models( $engineType ) {
1296 // This method is called by engines with just a string type
1297 // We need to get the models differently
1298 $options = $this->get_all_options();
1299 $engines = $options['ai_envs'];
1300 $models = [];
1301
1302 // Find all models for this engine type
1303 foreach ( $engines as $engine ) {
1304 if ( $engine['type'] === $engineType ) {
1305 if ( isset( $engine['models'] ) ) {
1306 foreach ( $engine['models'] as $model ) {
1307 $models[] = $model;
1308 }
1309 }
1310 }
1311 }
1312
1313 // Also check custom models
1314 if ( isset( $options['ai_models'] ) ) {
1315 foreach ( $options['ai_models'] as $model ) {
1316 if ( $model['type'] === $engineType ) {
1317 $models[] = $model;
1318 }
1319 }
1320 }
1321
1322 return $models;
1323 }
1324
1325 public function reset_options() {
1326 delete_option( $this->themes_option_name );
1327 delete_option( $this->chatbots_option_name );
1328 delete_option( $this->option_name );
1329 return $this->get_all_options( true );
1330 }
1331 #endregion
1332 }
1333