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