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