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