PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.2
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / core.php
ai-engine / classes Last commit date
data 1 year ago engines 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 1 year ago logging.php 1 year ago reply.php 10 months ago rest.php 10 months ago
core.php
1419 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.' );
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.' );
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 foreach ( $chatbots as &$chatbot ) {
954 foreach ( $chatbot as $key => &$value ) {
955 if ( in_array( $key, $deprecatedFields ) ) {
956 unset( $chatbot[$key] );
957 continue;
958 }
959 if ( in_array( $key, $htmlFields ) ) {
960 $value = wp_kses_post( $value );
961 }
962 else if ( in_array( $key, $whiteSpacedFields ) ) {
963 $value = sanitize_textarea_field( $value );
964 }
965 else if ( $key === 'functions' ) {
966 $functions = [];
967 foreach ( $value as $function ) {
968 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
969 $functions[] = [
970 'id' => sanitize_text_field( $function['id'] ),
971 'type' => sanitize_text_field( $function['type'] ),
972 ];
973 }
974 }
975 $value = $functions;
976 }
977 else if ( $key === 'mcpServers' ) {
978 $mcpServers = [];
979 foreach ( $value as $server ) {
980 if ( isset( $server['id'] ) ) {
981 $mcpServers[] = [
982 'id' => sanitize_text_field( $server['id'] ),
983 ];
984 }
985 }
986 $value = $mcpServers;
987 }
988 else if ( $key === 'tools' ) {
989 // Sanitize tools array (web_search, image_generation, thinking, etc)
990 $tools = [];
991 if ( is_array( $value ) ) {
992 foreach ( $value as $tool ) {
993 $sanitized_tool = sanitize_text_field( $tool );
994 if ( in_array( $sanitized_tool, ['web_search', 'image_generation', 'thinking', 'code_interpreter'] ) ) {
995 $tools[] = $sanitized_tool;
996 }
997 }
998 }
999 $value = $tools;
1000 }
1001 else if ( $key === 'crossSite' ) {
1002 // Handle crossSite object
1003 $crossSite = [
1004 'enabled' => isset( $value['enabled'] ) ? (bool) $value['enabled'] : false,
1005 'allowedDomains' => []
1006 ];
1007 if ( isset( $value['allowedDomains'] ) && is_array( $value['allowedDomains'] ) ) {
1008 foreach ( $value['allowedDomains'] as $domain ) {
1009 $sanitized_domain = sanitize_text_field( $domain );
1010 if ( !empty( $sanitized_domain ) ) {
1011 $crossSite['allowedDomains'][] = $sanitized_domain;
1012 }
1013 }
1014 }
1015 $value = $crossSite;
1016 }
1017 else {
1018 if ( in_array( $key, $keepLineReturnsFields ) ) {
1019 $value = preg_replace( '/\r\n/', '[==LINE_RETURN==]', $value );
1020 $value = preg_replace( '/\n/', '[==LINE_RETURN==]', $value );
1021 }
1022 $value = sanitize_text_field( $value );
1023 if ( in_array( $key, $keepLineReturnsFields ) ) {
1024 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1025 }
1026 }
1027 }
1028 }
1029 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1030 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1031 $chatbots = get_option( $this->chatbots_option_name, [] );
1032 return $chatbots;
1033 }
1034 return $chatbots;
1035 }
1036
1037 public function populate_dynamic_options( $options ) {
1038 static $populating = false;
1039
1040 // Prevent infinite recursion
1041 if ( $populating ) {
1042 return $options;
1043 }
1044
1045 $populating = true;
1046
1047 // Languages - use custom languages as the complete list
1048 $custom_languages = isset( $options['custom_languages'] ) && !empty( $options['custom_languages'] )
1049 ? $options['custom_languages']
1050 : [];
1051
1052 // If no custom languages defined, fall back to defaults
1053 if ( empty( $custom_languages ) ) {
1054 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1055 } else {
1056 // Process custom languages
1057 $processed_languages = [];
1058 foreach ( $custom_languages as $custom_lang ) {
1059 // Support formats like "Russian (ru)" or just "Russian"
1060 $custom_lang = trim( $custom_lang );
1061 if ( !empty( $custom_lang ) ) {
1062 // Check if language code is provided in parentheses
1063 if ( preg_match( '/^(.+)\s*\(([a-z]{2,3})\)$/i', $custom_lang, $matches ) ) {
1064 $lang_name = trim( $matches[1] );
1065 $lang_code = strtolower( trim( $matches[2] ) );
1066 $processed_languages[$lang_code] = $lang_name;
1067 } else {
1068 // No code provided, add as-is
1069 $processed_languages[] = $custom_lang;
1070 }
1071 }
1072 }
1073
1074 $options['languages'] = apply_filters( 'mwai_languages', $processed_languages );
1075 }
1076
1077 // Consolidate the Engines and their Models
1078 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1079 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1080 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1081 foreach ( $options['ai_engines'] as &$engine ) {
1082 if ( $engine['type'] === 'openai' ) {
1083 $engine['models'] = apply_filters(
1084 'mwai_openai_models',
1085 Meow_MWAI_Engines_OpenAI::get_models_static()
1086 );
1087 }
1088 else if ( $engine['type'] === 'anthropic' ) {
1089 $engine['models'] = apply_filters(
1090 'mwai_anthropic_models',
1091 Meow_MWAI_Engines_Anthropic::get_models_static()
1092 );
1093 }
1094 else if ( $engine['type'] === 'perplexity' ) {
1095 $engine['models'] = apply_filters(
1096 'mwai_perplexity_models',
1097 Meow_MWAI_Engines_Perplexity::get_models_static()
1098 );
1099 }
1100 else {
1101 $engine['models'] = [];
1102 foreach ( $options['ai_models'] as $model ) {
1103 if ( $model['type'] === $engine['type'] ) {
1104 $engine['models'][] = $model;
1105 }
1106 }
1107 }
1108 }
1109
1110 // Functions via Code Engine (or custom code)
1111 $json = [];
1112 $functions = apply_filters( 'mwai_functions_list', [] );
1113 foreach ( $functions as $function ) {
1114 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1115 }
1116 $options['functions'] = $json;
1117
1118 // Addons
1119 $options['addons'] = apply_filters( 'mwai_addons', [
1120 [
1121 'slug' => 'mwai-notifications',
1122 'name' => 'Notifications',
1123 'description' => 'Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.',
1124 'install_url' => 'https://meowapps.com/products/mwai-notifications/',
1125 'settings_url' => null,
1126 'stars' => 4,
1127 'enabled' => false
1128 ],
1129 [
1130 'slug' => 'mwai-ollama',
1131 'name' => 'Ollama',
1132 'description' => 'Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.',
1133 'install_url' => 'https://meowapps.com/products/mwai-ollama/',
1134 'settings_url' => null,
1135 'stars' => 3,
1136 'enabled' => false
1137 ],
1138 [
1139 'slug' => 'mwai-deepseek',
1140 'name' => 'DeepSeek',
1141 'description' => 'Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.',
1142 'install_url' => 'https://meowapps.com/products/deepseek/',
1143 'settings_url' => null,
1144 'stars' => 3,
1145 'enabled' => false
1146 ],
1147 [
1148 'slug' => 'mwai-websearch',
1149 'name' => 'Web Search',
1150 'description' => 'Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.',
1151 'install_url' => 'https://meowapps.com/products/mwai-websearch/',
1152 'settings_url' => null,
1153 'stars' => 5,
1154 'enabled' => false
1155 ],
1156 [
1157 'slug' => 'mwai-better-links',
1158 'name' => 'Better Links',
1159 'description' => 'Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.',
1160 'install_url' => 'https://meowapps.com/products/mwai-better-links/',
1161 'settings_url' => null,
1162 'stars' => 3,
1163 'enabled' => false
1164 ],
1165 [
1166 'slug' => 'mwai-woo-basics',
1167 'name' => 'Woo Basics',
1168 'description' => 'Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.',
1169 'install_url' => 'https://meowapps.com/products/mwai-woo-basics/',
1170 'settings_url' => null,
1171 'stars' => 2,
1172 'enabled' => false
1173 ],
1174 [
1175 'slug' => 'mwai-quick-actions',
1176 'name' => 'Quick Actions',
1177 'description' => 'Enable dynamic quick actions at chat start or during events, helping users find what they need faster.',
1178 'install_url' => 'https://meowapps.com/products/mwai-quick-actions/',
1179 'settings_url' => null,
1180 'stars' => 3,
1181 'enabled' => false
1182 ],
1183 [
1184 'slug' => 'mwai-content-parser',
1185 'name' => 'Content Parser',
1186 'description' => 'Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.',
1187 'install_url' => 'https://meowapps.com/products/mwai-content-parser/',
1188 'settings_url' => null,
1189 'stars' => 2,
1190 'enabled' => false
1191 ],
1192 [
1193 'slug' => 'mwai-visitor-form',
1194 'name' => 'Visitor Form',
1195 'description' => 'Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.',
1196 'install_url' => 'https://meowapps.com/products/mwai-visitor-form/',
1197 'settings_url' => null,
1198 'stars' => 2,
1199 'enabled' => false
1200 ],
1201 [
1202 'slug' => 'mwai-dynamic-keys',
1203 'name' => 'Dynamic Keys',
1204 'description' => 'Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.',
1205 'install_url' => 'https://meowapps.com/products/mwai-dynamic-keys/',
1206 'settings_url' => null,
1207 'stars' => 1,
1208 'enabled' => false
1209 ],
1210 ] );
1211
1212 // Populate usage data from ai_usage to ai_models_usage for the frontend
1213 $ai_usage = $this->get_option( 'ai_usage', [] );
1214 $options['ai_models_usage'] = $ai_usage;
1215
1216 // Also include daily usage data
1217 $ai_usage_daily = $this->get_option( 'ai_usage_daily', [] );
1218 $options['ai_models_usage_daily'] = $ai_usage_daily;
1219
1220 $populating = false;
1221 return $options;
1222 }
1223
1224 public function get_all_options( $force = false, $sanitize = false ) {
1225 if ( $force || is_null( $this->options ) ) {
1226 $options = get_option( $this->option_name, [] );
1227 $init_mode = empty( $options );
1228 foreach ( MWAI_OPTIONS as $key => $value ) {
1229 if ( !isset( $options[$key] ) ) {
1230 $options[$key] = $value;
1231 }
1232 }
1233 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1234 $options['default_limits'] = MWAI_LIMITS;
1235
1236 // Force sanitization if custom_languages is not set (migration)
1237 $needs_language_migration = !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] );
1238
1239 if ( $sanitize || $init_mode || $needs_language_migration ) {
1240 $options = $this->sanitize_options( $options );
1241 }
1242 $this->options = $options;
1243 }
1244 $options = $this->populate_dynamic_options( $this->options );
1245 return $options;
1246 }
1247
1248 // Sanitize options when we update the plugin or perform some updates
1249 // if we change the structure of the options.
1250 public function sanitize_options( $options ) {
1251 $needs_update = false;
1252
1253 // Removing old options of options renaming should be done here, as it was done before.
1254 // Check version 2.6.8 for an example.
1255
1256 // Avoid the logs_path to be a PHP file.
1257 if ( isset( $options['logs_path'] ) ) {
1258 $logs_path = $options['logs_path'];
1259 if ( substr( $logs_path, -4 ) !== '.log' ) {
1260 $options['logs_path'] = '';
1261 $needs_update = true;
1262 }
1263 }
1264
1265 // The IDs for the embeddings environments are generated here.
1266 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1267 $embeddings_default_exists = false;
1268 if ( isset( $options['embeddings_envs'] ) ) {
1269 foreach ( $options['embeddings_envs'] as &$env ) {
1270 if ( !isset( $env['id'] ) ) {
1271 $env['id'] = $this->get_random_id();
1272 $needs_update = true;
1273 }
1274 if ( $env['id'] === $options['embeddings_default_env'] ) {
1275 $embeddings_default_exists = true;
1276 }
1277 }
1278 }
1279 if ( !$embeddings_default_exists ) {
1280 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1281 $needs_update = true;
1282 }
1283
1284 // The IDs for the AI environments are generated here.
1285 $allEnvIds = [];
1286 $ai_default_exists = false;
1287 if ( isset( $options['ai_envs'] ) ) {
1288 foreach ( $options['ai_envs'] as &$env ) {
1289 if ( !isset( $env['id'] ) ) {
1290 $env['id'] = $this->get_random_id();
1291 $needs_update = true;
1292 }
1293 if ( $env['id'] === $options['ai_default_env'] ) {
1294 $ai_default_exists = true;
1295 }
1296 $allEnvIds[] = $env['id'];
1297 }
1298 }
1299 if ( !$ai_default_exists ) {
1300 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1301 $needs_update = true;
1302 }
1303
1304 // The IDs for the MCP environments are generated here.
1305 if ( isset( $options['mcp_envs'] ) ) {
1306 foreach ( $options['mcp_envs'] as &$env ) {
1307 if ( !isset( $env['id'] ) ) {
1308 $env['id'] = $this->get_random_id();
1309 $needs_update = true;
1310 }
1311 }
1312 }
1313
1314 // All the models with an envId that does not exist anymore are removed.
1315 if ( isset( $options['ai_models'] ) ) {
1316 $options['ai_models'] = array_values( array_filter(
1317 $options['ai_models'],
1318 function ( $model ) use ( $allEnvIds, &$needs_update ) {
1319 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1320 $needs_update = true;
1321 return false;
1322 }
1323 return true;
1324 }
1325 ) );
1326 }
1327
1328 // Migration: Populate custom_languages if empty for existing installations
1329 if ( !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] ) ) {
1330 $options['custom_languages'] = [
1331 'English (en)',
1332 'German (de)',
1333 'French (fr)',
1334 'Spanish (es)',
1335 'Italian (it)',
1336 'Chinese (zh)',
1337 'Japanese (ja)',
1338 'Portuguese (pt)'
1339 ];
1340 $needs_update = true;
1341 }
1342
1343 if ( $needs_update ) {
1344 ksort( $options );
1345 update_option( $this->option_name, $options, false );
1346 }
1347
1348 return $options;
1349 }
1350
1351 public function update_options( $options ) {
1352 if ( !update_option( $this->option_name, $options, false ) ) {
1353 return false;
1354 }
1355 $options = $this->get_all_options( true, true );
1356 return $options;
1357 }
1358
1359 public function update_option( $option, $value ) {
1360 $options = $this->get_all_options( true );
1361 $options[$option] = $value;
1362 return $this->update_options( $options );
1363 }
1364
1365 public function get_option( $option, $default = null ) {
1366 $options = $this->get_all_options();
1367 return $options[$option] ?? $default;
1368 }
1369
1370 public function update_ai_env( $env_id, $option, $value ) {
1371 $options = $this->get_all_options( true );
1372 foreach ( $options['ai_envs'] as &$env ) {
1373 if ( $env['id'] === $env_id ) {
1374 $env[$option] = $value;
1375 break;
1376 }
1377 }
1378 return $this->update_options( $options );
1379 }
1380
1381 public function get_engine_models( $engineType ) {
1382 // This method is called by engines with just a string type
1383 // We need to get the models differently
1384 $options = $this->get_all_options();
1385 $engines = $options['ai_envs'];
1386 $models = [];
1387
1388 // Find all models for this engine type
1389 foreach ( $engines as $engine ) {
1390 if ( $engine['type'] === $engineType ) {
1391 if ( isset( $engine['models'] ) ) {
1392 foreach ( $engine['models'] as $model ) {
1393 $models[] = $model;
1394 }
1395 }
1396 }
1397 }
1398
1399 // Also check custom models
1400 if ( isset( $options['ai_models'] ) ) {
1401 foreach ( $options['ai_models'] as $model ) {
1402 if ( $model['type'] === $engineType ) {
1403 $models[] = $model;
1404 }
1405 }
1406 }
1407
1408 return $models;
1409 }
1410
1411 public function reset_options() {
1412 delete_option( $this->themes_option_name );
1413 delete_option( $this->chatbots_option_name );
1414 delete_option( $this->option_name );
1415 return $this->get_all_options( true );
1416 }
1417 #endregion
1418 }
1419