PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.1
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.1
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
1285 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 $post = get_post( $postId );
306 if ( !$post ) {
307 return false;
308 }
309 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
310 $pattern = '/\[mwai_.*?\]/';
311 $text = preg_replace( $pattern, '', $text );
312 if ( $this->get_option( 'resolve_shortcodes' ) ) {
313 $text = apply_filters( 'the_content', $text );
314 }
315 else {
316 $pattern = "/\[[^\]]+\]/";
317 $text = preg_replace( $pattern, '', $text );
318 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
319 $text = preg_replace( $pattern, '', $text );
320 }
321 $text = $this->clean_text( $text );
322 $text = $this->clean_sentences( $text );
323 $text = apply_filters( 'mwai_post_content', $text, $postId );
324 return $text;
325 }
326
327 public function markdown_to_html( $content ) {
328 $Parsedown = new Parsedown();
329 $content = $Parsedown->text( $content );
330 return $content;
331 }
332
333 public function get_post_language( $postId ) {
334 $locale = get_locale();
335 $code = strtolower( substr( $locale, 0, 2 ) );
336 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
337 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
338 if ( !empty( $lang ) ) {
339 $locale = $lang['locale'];
340 $humanLanguage = $lang['display_name'];
341 }
342 return strtolower( "$locale ($humanLanguage)" );
343 }
344
345 public function do_placeholders( $text ) {
346 $defaultPlaceholders = [];
347 $dataPlaceholders = $this->get_user_data();
348 if ( !empty( $dataPlaceholders ) ) {
349 $defaultPlaceholders = array_merge( $defaultPlaceholders, $dataPlaceholders );
350 }
351 $placeholders = apply_filters( 'mwai_placeholders', $defaultPlaceholders );
352 foreach ( $placeholders as $key => $value ) {
353 $text = str_replace( '{' . $key . '}', $value, $text );
354 }
355 return $text;
356 }
357 #endregion
358
359 #region Image-Related Helpers
360 public static function is_image( $file ) {
361 global $mwai_core;
362 if ( $mwai_core && $mwai_core->imageService ) {
363 return $mwai_core->imageService->is_image( $file );
364 }
365 // Fallback to original implementation if service not available
366 $mimeType = self::get_mime_type( $file );
367 if ( strpos( $mimeType, 'image' ) !== false ) {
368 return true;
369 }
370 return false;
371 }
372
373 public static function get_image_resolution( $url ) {
374 global $mwai_core;
375 if ( $mwai_core && $mwai_core->imageService ) {
376 return $mwai_core->imageService->get_image_resolution( $url );
377 }
378 // Fallback to original implementation if service not available
379 if ( empty( $url ) ) {
380 return null;
381 }
382 $headers = get_headers( $url, 1 );
383 if ( strpos( $headers[0], '200' ) === false ) {
384 return null;
385 }
386 $image_info = getimagesize( $url );
387 if ( $image_info === false ) {
388 return null;
389 }
390 return [
391 'width' => $image_info[0],
392 'height' => $image_info[1]
393 ];
394 }
395
396 public static function get_mime_type( $file ) {
397 global $mwai_core;
398 if ( $mwai_core && $mwai_core->imageService ) {
399 return $mwai_core->imageService->get_mime_type( $file );
400 }
401
402 // Fallback implementation - this should rarely be used as imageService is initialized early
403 Meow_MWAI_Logging::warn( 'get_mime_type called before imageService is available' );
404
405 // Basic extension-based detection only
406 $extension = pathinfo( $file, PATHINFO_EXTENSION );
407 $extension = strtolower( $extension );
408 $mimeTypes = [
409 'jpg' => 'image/jpeg',
410 'jpeg' => 'image/jpeg',
411 'png' => 'image/png',
412 'gif' => 'image/gif',
413 'webp' => 'image/webp',
414 'bmp' => 'image/bmp',
415 'tiff' => 'image/tiff',
416 'tif' => 'image/tiff',
417 'svg' => 'image/svg+xml',
418 'ico' => 'image/x-icon',
419 'pdf' => 'application/pdf',
420 ];
421 return isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
422 }
423
424 public function download_image( $url ) {
425 return $this->imageService->download_image( $url );
426 }
427
428 /**
429 * Add an image from a URL to the Media Library.
430 * @param string $url The URL of the image to be downloaded.
431 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
432 * @param string $title The title of the image.
433 * @param string $description The description of the image.
434 * @param string $caption The caption of the image.
435 * @param string $alt The alt text of the image.
436 * @return int The attachment ID of the image.
437 */
438 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null ) {
439 return $this->imageService->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, $attachedPost );
440 }
441 #endregion
442
443 #region Context-Related Helpers
444 public function retrieve_context( $params, $query, $streamCallback = null ) {
445 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
446 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
447
448 $context = apply_filters( 'mwai_context_search', [], $query, [
449 'embeddingsEnvId' => $embeddingsEnvId,
450 'streamCallback' => $streamCallback
451 ] );
452
453 // Emit embeddings event if streaming and context was found
454 if ( $streamCallback && !empty( $context ) ) {
455 $count = 0;
456 if ( isset( $context['embeddings'] ) && is_array( $context['embeddings'] ) ) {
457 $count = count( $context['embeddings'] );
458 }
459 else if ( isset( $context['content'] ) ) {
460 $count = 1;
461 }
462 if ( $count > 0 ) {
463 $event = Meow_MWAI_Event::embeddings( $count );
464 $streamCallback( $event );
465 }
466 }
467
468 if ( empty( $context ) ) {
469 return null;
470 }
471 else if ( !isset( $context['content'] ) ) {
472 Meow_MWAI_Logging::warn( 'A context without content was returned.' );
473 return null;
474 }
475 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
476 $context['length'] = strlen( $context['content'] );
477 return $context;
478 }
479 #endregion
480
481 #region Users/Sessions Helpers
482
483 public function get_nonce( $force = false ) {
484 return $this->sessionService->get_nonce( $force );
485 }
486
487 // This is a bit hacky, but chatId needs to be retrieved or generated.
488 // Maybe we can clean this up later.
489 public function fix_chat_id( $query, $params ) {
490 return $this->sessionService->fix_chat_id( $query, $params );
491 }
492
493 public function get_session_id() {
494 return $this->sessionService->get_session_id();
495 }
496
497 /**
498 * Get the Response ID Manager service
499 */
500 public function get_response_id_manager() {
501 return $this->responseIdManager;
502 }
503
504 /**
505 * Get the Message Builder service
506 */
507 public function get_message_builder() {
508 return $this->messageBuilder;
509 }
510
511 // Get the UserID from the data, or from the current user
512 public function get_user_id( $data = null ) {
513 return $this->sessionService->get_user_id( $data );
514 }
515
516 public function get_admin_user() {
517 return $this->sessionService->get_admin_user();
518 }
519
520 public function get_user_data() {
521 return $this->sessionService->get_user_data();
522 }
523
524 public function get_ip_address( $force = false ) {
525 return $this->sessionService->get_ip_address( $force );
526 }
527
528 #endregion
529
530 #region Sanitization
531 public function sanitize_sort(
532 &$sort,
533 $default_accessor = 'created',
534 $default_order = 'DESC',
535 $allowed_columns = [ 'created', 'updated', 'name', 'id', 'time', 'units', 'price' ]
536 ) {
537
538 // Ensure $sort is an array
539 if ( !is_array( $sort ) ) {
540 $sort = [ 'accessor' => $default_accessor, 'by' => $default_order ];
541 }
542 // Extract and sanitize the accessor
543 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
544 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
545 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
546 $sort_accessor = $default_accessor;
547 }
548 // Extract and sanitize the sort order
549 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
550 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
551 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
552 $sort_by = $default_order;
553 }
554 // Update the sort array with sanitized values
555 $sort['accessor'] = $sort_accessor;
556 $sort['by'] = $sort_by;
557 }
558 #endregion
559
560 #region Other Helpers
561 public function safe_strlen( $string, $encoding = 'UTF-8' ) {
562 if ( function_exists( 'mb_strlen' ) ) {
563 return mb_strlen( $string, $encoding );
564 }
565 else {
566 // Fallback implementation for environments without mbstring extension
567 return preg_match_all( '/./u', $string, $matches );
568 }
569 }
570
571 public function check_rest_nonce( $request ) {
572 // REST NONCE VERIFICATION:
573 // Validates nonce from X-WP-Nonce header using WordPress nonce system.
574 // Returns: false (invalid), 1 (0-12 hours old), or 2 (12-24 hours old)
575 // WordPress REST permission callbacks accept any truthy value as success.
576 // The filter allows custom authorization logic if needed.
577 $nonce = $request->get_header( 'X-WP-Nonce' );
578 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
579 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
580 }
581
582 public function get_random_id( $length = 8, $excludeIds = [] ) {
583 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
584 $charactersLength = strlen( $characters );
585 $randomId = '';
586 for ( $i = 0; $i < $length; $i++ ) {
587 $randomId .= $characters[ mt_rand( 0, $charactersLength - 1 ) ];
588 }
589 if ( in_array( $randomId, $excludeIds ) ) {
590 return $this->get_random_id( $length, $excludeIds );
591 }
592 return $randomId;
593 }
594
595 public function is_url( $url ) {
596 return strpos( $url, 'http' ) === 0 ? true : false;
597 }
598
599 public function get_post_types() {
600 $excluded = [ 'attachment', 'revision', 'nav_menu_item' ];
601 $post_types = [];
602 $types = get_post_types( [], 'objects' );
603
604 // Let's get the Post Types that are enabled for Embeddings Sync
605 $embeddingsSettings = $this->get_option( 'embeddings' );
606 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
607
608 foreach ( $types as $type ) {
609 $forced = in_array( $type->name, $syncPostTypes );
610 // Should not be excluded.
611 if ( !$forced && in_array( $type->name, $excluded ) ) {
612 continue;
613 }
614 // Should be public.
615 if ( !$forced && !$type->public ) {
616 continue;
617 }
618 $post_types[] = [
619 'name' => $type->labels->name,
620 'type' => $type->name,
621 ];
622 }
623
624 // Let's get the Post Types that are enabled for Embeddings Sync
625 $embeddingsSettings = $this->get_option( 'embeddings' );
626 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
627
628 return $post_types;
629 }
630
631 public function get_post( $post ) {
632 if ( is_numeric( $post ) ) {
633 $post = get_post( $post );
634 }
635 if ( is_object( $post ) ) {
636 $post = (array) $post;
637 }
638 if ( !is_array( $post ) ) {
639 return null;
640 }
641 $language = $this->get_post_language( $post['ID'] );
642 $content = $this->get_post_content( $post['ID'] );
643 $title = $post['post_title'];
644 $excerpt = $post['post_excerpt'];
645 $url = get_permalink( $post['ID'] );
646 $checksum = wp_hash( $content . $title . $url );
647 return [
648 'postId' => (int) $post['ID'],
649 'title' => $title,
650 'content' => $content,
651 'excerpt' => $excerpt,
652 'url' => $url,
653 'language' => $language ?? 'english',
654 'checksum' => $checksum,
655 ];
656 }
657 #endregion
658
659 #region Usage & Costs
660
661 // Quick and dirty token estimation
662 // Let's keep this synchronized with Helpers in JS
663 public static function estimate_tokens( ...$args ): int {
664 global $mwai_core;
665 if ( $mwai_core && $mwai_core->usageStatsService ) {
666 return $mwai_core->usageStatsService->estimate_tokens( ...$args );
667 }
668 // Fallback to original implementation if service not available
669 $text = '';
670 foreach ( $args as $arg ) {
671 if ( is_array( $arg ) ) {
672 foreach ( $arg as $message ) {
673 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : '';
674 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : '';
675 }
676 }
677 else if ( is_string( $arg ) ) {
678 $text .= $arg;
679 }
680 }
681 $averageTokenLength = 4;
682 $words = preg_split( '/\s+/', trim( $text ) );
683 $tokenCount = 0;
684 foreach ( $words as $word ) {
685 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
686 }
687 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
688 }
689
690 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
691 return $this->usageStatsService->record_tokens_usage( $model, $in_tokens, $out_tokens, $returned_price );
692 }
693
694 public function record_audio_usage( $model, $seconds ) {
695 return $this->usageStatsService->record_audio_usage( $model, $seconds );
696 }
697
698 public function record_images_usage( $model, $resolution, $images ) {
699 return $this->usageStatsService->record_images_usage( $model, $resolution, $images );
700 }
701
702 #endregion
703
704 #region Streaming
705 public function stream_push( $data, $query = null ) {
706 // Handle new Event objects
707 if ( is_object( $data ) && method_exists( $data, 'to_array' ) ) {
708 $data = $data->to_array();
709 }
710
711 $data = apply_filters( 'mwai_stream_push', $data, $query );
712 $out = 'data: ' . json_encode( $data );
713 echo $out;
714 echo "\n\n";
715 if ( ob_get_level() > 0 ) {
716 ob_end_flush();
717 }
718 flush();
719 }
720 #endregion
721
722 #region Options
723 public function get_themes() {
724 $themes = get_option( $this->themes_option_name, [] );
725 $themes = empty( $themes ) ? [] : $themes;
726
727 $internalThemes = [
728 'chatgpt' => [
729 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
730 'settings' => [], 'style' => ''
731 ],
732 'messages' => [
733 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
734 'settings' => [], 'style' => ''
735 ],
736 'timeless' => [
737 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
738 'settings' => [], 'style' => ''
739 ],
740 ];
741 $customThemes = [];
742 foreach ( $themes as $theme ) {
743 if ( isset( $internalThemes[$theme['themeId']] ) ) {
744 $internalThemes[$theme['themeId']] = $theme;
745 continue;
746 }
747 $customThemes[] = $theme;
748 }
749 return array_merge( array_values( $internalThemes ), $customThemes );
750 }
751
752 public function update_themes( $themes ) {
753 update_option( $this->themes_option_name, $themes );
754 return $themes;
755 }
756
757 public function get_chatbots() {
758 $chatbots = get_option( $this->chatbots_option_name, [] );
759 $hasChanges = false;
760 if ( empty( $chatbots ) ) {
761 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
762 }
763 $hasDefault = false;
764 foreach ( $chatbots as &$chatbot ) {
765 if ( $chatbot['botId'] === 'default' ) {
766 $hasDefault = true;
767 }
768 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
769 // Use default value if not set.
770 if ( !isset( $chatbot[$key] ) ) {
771 $chatbot[$key] = $value;
772 }
773 }
774
775 /*
776 This is the best section to rename fields.
777 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).
778 */
779
780 // if ( isset( $chatbot['context'] ) ) {
781 // $chatbot['instructions'] = $chatbot['context'];
782 // unset( $chatbot['context'] );
783 // $hasChanges = true;
784 // }
785 }
786 if ( !$hasDefault ) {
787 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
788 array_unshift( $chatbots, $defaultBot );
789 $hasChanges = true;
790 }
791 if ( $hasChanges ) {
792 update_option( $this->chatbots_option_name, $chatbots );
793 }
794 return $chatbots;
795 }
796
797 public function get_chatbot( $botId ) {
798 $chatbots = $this->get_chatbots();
799 foreach ( $chatbots as $chatbot ) {
800 if ( $chatbot['botId'] === (string) $botId ) {
801 return $chatbot;
802 }
803 }
804 return null;
805 }
806
807 public function get_embeddings_env( $envId ) {
808 return $this->modelEnvironmentService->get_embeddings_env( $envId );
809 }
810
811 public function get_ai_env( $envId ) {
812 return $this->modelEnvironmentService->get_ai_env( $envId );
813 }
814
815 public function get_assistant( $envId, $assistantId ) {
816 return $this->modelEnvironmentService->get_assistant( $envId, $assistantId );
817 }
818
819 public function get_theme( $themeId ) {
820 $themes = $this->get_themes();
821 foreach ( $themes as $theme ) {
822 if ( $theme['themeId'] === $themeId ) {
823 return $theme;
824 }
825 }
826 return null;
827 }
828
829 public function update_chatbots( $chatbots ) {
830 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
831 // TODO: I think some HTML fields are missing, guestName, maybe others.
832 $htmlFields = [ 'instructions', 'textCompliance', 'aiName', 'userName', 'startSentence' ];
833 $keepLineReturnsFields = [ 'instructions' ];
834 $whiteSpacedFields = [ 'context' ];
835 foreach ( $chatbots as &$chatbot ) {
836 foreach ( $chatbot as $key => &$value ) {
837 if ( in_array( $key, $deprecatedFields ) ) {
838 unset( $chatbot[$key] );
839 continue;
840 }
841 if ( in_array( $key, $htmlFields ) ) {
842 $value = wp_kses_post( $value );
843 }
844 else if ( in_array( $key, $whiteSpacedFields ) ) {
845 $value = sanitize_textarea_field( $value );
846 }
847 else if ( $key === 'functions' ) {
848 $functions = [];
849 foreach ( $value as $function ) {
850 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
851 $functions[] = [
852 'id' => sanitize_text_field( $function['id'] ),
853 'type' => sanitize_text_field( $function['type'] ),
854 ];
855 }
856 }
857 $value = $functions;
858 }
859 else if ( $key === 'mcpServers' ) {
860 $mcpServers = [];
861 foreach ( $value as $server ) {
862 if ( isset( $server['id'] ) ) {
863 $mcpServers[] = [
864 'id' => sanitize_text_field( $server['id'] ),
865 ];
866 }
867 }
868 $value = $mcpServers;
869 }
870 else if ( $key === 'tools' ) {
871 // Sanitize tools array (web_search, image_generation, thinking, etc)
872 $tools = [];
873 if ( is_array( $value ) ) {
874 foreach ( $value as $tool ) {
875 $sanitized_tool = sanitize_text_field( $tool );
876 if ( in_array( $sanitized_tool, ['web_search', 'image_generation', 'thinking'] ) ) {
877 $tools[] = $sanitized_tool;
878 }
879 }
880 }
881 $value = $tools;
882 }
883 else {
884 if ( in_array( $key, $keepLineReturnsFields ) ) {
885 $value = preg_replace( '/\r\n/', '[==LINE_RETURN==]', $value );
886 $value = preg_replace( '/\n/', '[==LINE_RETURN==]', $value );
887 }
888 $value = sanitize_text_field( $value );
889 if ( in_array( $key, $keepLineReturnsFields ) ) {
890 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
891 }
892 }
893 }
894 }
895 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
896 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
897 $chatbots = get_option( $this->chatbots_option_name, [] );
898 return $chatbots;
899 }
900 return $chatbots;
901 }
902
903 public function populate_dynamic_options( $options ) {
904 static $populating = false;
905
906 // Prevent infinite recursion
907 if ( $populating ) {
908 return $options;
909 }
910
911 $populating = true;
912
913 // Languages - use custom languages as the complete list
914 $custom_languages = isset( $options['custom_languages'] ) && !empty( $options['custom_languages'] )
915 ? $options['custom_languages']
916 : [];
917
918 // If no custom languages defined, fall back to defaults
919 if ( empty( $custom_languages ) ) {
920 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
921 } else {
922 // Process custom languages
923 $processed_languages = [];
924 foreach ( $custom_languages as $custom_lang ) {
925 // Support formats like "Russian (ru)" or just "Russian"
926 $custom_lang = trim( $custom_lang );
927 if ( !empty( $custom_lang ) ) {
928 // Check if language code is provided in parentheses
929 if ( preg_match( '/^(.+)\s*\(([a-z]{2,3})\)$/i', $custom_lang, $matches ) ) {
930 $lang_name = trim( $matches[1] );
931 $lang_code = strtolower( trim( $matches[2] ) );
932 $processed_languages[$lang_code] = $lang_name;
933 } else {
934 // No code provided, add as-is
935 $processed_languages[] = $custom_lang;
936 }
937 }
938 }
939
940 $options['languages'] = apply_filters( 'mwai_languages', $processed_languages );
941 }
942
943 // Consolidate the Engines and their Models
944 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
945 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
946 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
947 foreach ( $options['ai_engines'] as &$engine ) {
948 if ( $engine['type'] === 'openai' ) {
949 $engine['models'] = apply_filters(
950 'mwai_openai_models',
951 Meow_MWAI_Engines_OpenAI::get_models_static()
952 );
953 }
954 else if ( $engine['type'] === 'anthropic' ) {
955 $engine['models'] = apply_filters(
956 'mwai_anthropic_models',
957 Meow_MWAI_Engines_Anthropic::get_models_static()
958 );
959 }
960 else if ( $engine['type'] === 'perplexity' ) {
961 $engine['models'] = apply_filters(
962 'mwai_perplexity_models',
963 Meow_MWAI_Engines_Perplexity::get_models_static()
964 );
965 }
966 else {
967 $engine['models'] = [];
968 foreach ( $options['ai_models'] as $model ) {
969 if ( $model['type'] === $engine['type'] ) {
970 $engine['models'][] = $model;
971 }
972 }
973 }
974 }
975
976 // Functions via Code Engine (or custom code)
977 $json = [];
978 $functions = apply_filters( 'mwai_functions_list', [] );
979 foreach ( $functions as $function ) {
980 $json[] = Meow_MWAI_Query_Function::toJson( $function );
981 }
982 $options['functions'] = $json;
983
984 // Addons
985 $options['addons'] = apply_filters( 'mwai_addons', [
986 [
987 'slug' => 'mwai-notifications',
988 'name' => 'Notifications',
989 'description' => 'Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.',
990 'install_url' => 'https://meowapps.com/products/mwai-notifications/',
991 'settings_url' => null,
992 'stars' => 4,
993 'enabled' => false
994 ],
995 [
996 'slug' => 'mwai-ollama',
997 'name' => 'Ollama',
998 'description' => 'Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.',
999 'install_url' => 'https://meowapps.com/products/mwai-ollama/',
1000 'settings_url' => null,
1001 'stars' => 3,
1002 'enabled' => false
1003 ],
1004 [
1005 'slug' => 'mwai-deepseek',
1006 'name' => 'DeepSeek',
1007 'description' => 'Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.',
1008 'install_url' => 'https://meowapps.com/products/deepseek/',
1009 'settings_url' => null,
1010 'stars' => 3,
1011 'enabled' => false
1012 ],
1013 [
1014 'slug' => 'mwai-websearch',
1015 'name' => 'Web Search',
1016 'description' => 'Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.',
1017 'install_url' => 'https://meowapps.com/products/mwai-websearch/',
1018 'settings_url' => null,
1019 'stars' => 5,
1020 'enabled' => false
1021 ],
1022 [
1023 'slug' => 'mwai-better-links',
1024 'name' => 'Better Links',
1025 'description' => 'Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.',
1026 'install_url' => 'https://meowapps.com/products/mwai-better-links/',
1027 'settings_url' => null,
1028 'stars' => 3,
1029 'enabled' => false
1030 ],
1031 [
1032 'slug' => 'mwai-woo-basics',
1033 'name' => 'Woo Basics',
1034 'description' => 'Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.',
1035 'install_url' => 'https://meowapps.com/products/mwai-woo-basics/',
1036 'settings_url' => null,
1037 'stars' => 2,
1038 'enabled' => false
1039 ],
1040 [
1041 'slug' => 'mwai-quick-actions',
1042 'name' => 'Quick Actions',
1043 'description' => 'Enable dynamic quick actions at chat start or during events, helping users find what they need faster.',
1044 'install_url' => 'https://meowapps.com/products/mwai-quick-actions/',
1045 'settings_url' => null,
1046 'stars' => 3,
1047 'enabled' => false
1048 ],
1049 [
1050 'slug' => 'mwai-content-parser',
1051 'name' => 'Content Parser',
1052 'description' => 'Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.',
1053 'install_url' => 'https://meowapps.com/products/mwai-content-parser/',
1054 'settings_url' => null,
1055 'stars' => 2,
1056 'enabled' => false
1057 ],
1058 [
1059 'slug' => 'mwai-visitor-form',
1060 'name' => 'Visitor Form',
1061 'description' => 'Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.',
1062 'install_url' => 'https://meowapps.com/products/mwai-visitor-form/',
1063 'settings_url' => null,
1064 'stars' => 2,
1065 'enabled' => false
1066 ],
1067 [
1068 'slug' => 'mwai-dynamic-keys',
1069 'name' => 'Dynamic Keys',
1070 'description' => 'Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.',
1071 'install_url' => 'https://meowapps.com/products/mwai-dynamic-keys/',
1072 'settings_url' => null,
1073 'stars' => 1,
1074 'enabled' => false
1075 ],
1076 ] );
1077
1078 // Populate usage data from ai_usage to ai_models_usage for the frontend
1079 $ai_usage = $this->get_option( 'ai_usage', [] );
1080 $options['ai_models_usage'] = $ai_usage;
1081
1082 // Also include daily usage data
1083 $ai_usage_daily = $this->get_option( 'ai_usage_daily', [] );
1084 $options['ai_models_usage_daily'] = $ai_usage_daily;
1085
1086 $populating = false;
1087 return $options;
1088 }
1089
1090 public function get_all_options( $force = false, $sanitize = false ) {
1091 if ( $force || is_null( $this->options ) ) {
1092 $options = get_option( $this->option_name, [] );
1093 $init_mode = empty( $options );
1094 foreach ( MWAI_OPTIONS as $key => $value ) {
1095 if ( !isset( $options[$key] ) ) {
1096 $options[$key] = $value;
1097 }
1098 }
1099 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1100 $options['default_limits'] = MWAI_LIMITS;
1101
1102 // Force sanitization if custom_languages is not set (migration)
1103 $needs_language_migration = !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] );
1104
1105 if ( $sanitize || $init_mode || $needs_language_migration ) {
1106 $options = $this->sanitize_options( $options );
1107 }
1108 $this->options = $options;
1109 }
1110 $options = $this->populate_dynamic_options( $this->options );
1111 return $options;
1112 }
1113
1114 // Sanitize options when we update the plugin or perform some updates
1115 // if we change the structure of the options.
1116 public function sanitize_options( $options ) {
1117 $needs_update = false;
1118
1119 // Removing old options of options renaming should be done here, as it was done before.
1120 // Check version 2.6.8 for an example.
1121
1122 // Avoid the logs_path to be a PHP file.
1123 if ( isset( $options['logs_path'] ) ) {
1124 $logs_path = $options['logs_path'];
1125 if ( substr( $logs_path, -4 ) !== '.log' ) {
1126 $options['logs_path'] = '';
1127 $needs_update = true;
1128 }
1129 }
1130
1131 // The IDs for the embeddings environments are generated here.
1132 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1133 $embeddings_default_exists = false;
1134 if ( isset( $options['embeddings_envs'] ) ) {
1135 foreach ( $options['embeddings_envs'] as &$env ) {
1136 if ( !isset( $env['id'] ) ) {
1137 $env['id'] = $this->get_random_id();
1138 $needs_update = true;
1139 }
1140 if ( $env['id'] === $options['embeddings_default_env'] ) {
1141 $embeddings_default_exists = true;
1142 }
1143 }
1144 }
1145 if ( !$embeddings_default_exists ) {
1146 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1147 $needs_update = true;
1148 }
1149
1150 // The IDs for the AI environments are generated here.
1151 $allEnvIds = [];
1152 $ai_default_exists = false;
1153 if ( isset( $options['ai_envs'] ) ) {
1154 foreach ( $options['ai_envs'] as &$env ) {
1155 if ( !isset( $env['id'] ) ) {
1156 $env['id'] = $this->get_random_id();
1157 $needs_update = true;
1158 }
1159 if ( $env['id'] === $options['ai_default_env'] ) {
1160 $ai_default_exists = true;
1161 }
1162 $allEnvIds[] = $env['id'];
1163 }
1164 }
1165 if ( !$ai_default_exists ) {
1166 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1167 $needs_update = true;
1168 }
1169
1170 // The IDs for the MCP environments are generated here.
1171 if ( isset( $options['mcp_envs'] ) ) {
1172 foreach ( $options['mcp_envs'] as &$env ) {
1173 if ( !isset( $env['id'] ) ) {
1174 $env['id'] = $this->get_random_id();
1175 $needs_update = true;
1176 }
1177 }
1178 }
1179
1180 // All the models with an envId that does not exist anymore are removed.
1181 if ( isset( $options['ai_models'] ) ) {
1182 $options['ai_models'] = array_values( array_filter(
1183 $options['ai_models'],
1184 function ( $model ) use ( $allEnvIds, &$needs_update ) {
1185 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1186 $needs_update = true;
1187 return false;
1188 }
1189 return true;
1190 }
1191 ) );
1192 }
1193
1194 // Migration: Populate custom_languages if empty for existing installations
1195 if ( !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] ) ) {
1196 $options['custom_languages'] = [
1197 'English (en)',
1198 'German (de)',
1199 'French (fr)',
1200 'Spanish (es)',
1201 'Italian (it)',
1202 'Chinese (zh)',
1203 'Japanese (ja)',
1204 'Portuguese (pt)'
1205 ];
1206 $needs_update = true;
1207 }
1208
1209 if ( $needs_update ) {
1210 ksort( $options );
1211 update_option( $this->option_name, $options, false );
1212 }
1213
1214 return $options;
1215 }
1216
1217 public function update_options( $options ) {
1218 if ( !update_option( $this->option_name, $options, false ) ) {
1219 return false;
1220 }
1221 $options = $this->get_all_options( true, true );
1222 return $options;
1223 }
1224
1225 public function update_option( $option, $value ) {
1226 $options = $this->get_all_options( true );
1227 $options[$option] = $value;
1228 return $this->update_options( $options );
1229 }
1230
1231 public function get_option( $option, $default = null ) {
1232 $options = $this->get_all_options();
1233 return $options[$option] ?? $default;
1234 }
1235
1236 public function update_ai_env( $env_id, $option, $value ) {
1237 $options = $this->get_all_options( true );
1238 foreach ( $options['ai_envs'] as &$env ) {
1239 if ( $env['id'] === $env_id ) {
1240 $env[$option] = $value;
1241 break;
1242 }
1243 }
1244 return $this->update_options( $options );
1245 }
1246
1247 public function get_engine_models( $engineType ) {
1248 // This method is called by engines with just a string type
1249 // We need to get the models differently
1250 $options = $this->get_all_options();
1251 $engines = $options['ai_envs'];
1252 $models = [];
1253
1254 // Find all models for this engine type
1255 foreach ( $engines as $engine ) {
1256 if ( $engine['type'] === $engineType ) {
1257 if ( isset( $engine['models'] ) ) {
1258 foreach ( $engine['models'] as $model ) {
1259 $models[] = $model;
1260 }
1261 }
1262 }
1263 }
1264
1265 // Also check custom models
1266 if ( isset( $options['ai_models'] ) ) {
1267 foreach ( $options['ai_models'] as $model ) {
1268 if ( $model['type'] === $engineType ) {
1269 $models[] = $model;
1270 }
1271 }
1272 }
1273
1274 return $models;
1275 }
1276
1277 public function reset_options() {
1278 delete_option( $this->themes_option_name );
1279 delete_option( $this->chatbots_option_name );
1280 delete_option( $this->option_name );
1281 return $this->get_all_options( true );
1282 }
1283 #endregion
1284 }
1285