PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.7.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.7.9
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
engines 1 year ago modules 1 year ago queries 1 year ago admin.php 1 year ago api.php 1 year ago core.php 1 year ago discussion.php 1 year ago init.php 1 year ago logging.php 1 year ago reply.php 1 year ago rest.php 1 year ago
core.php
1329 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 {
14 public $admin = null;
15 public $is_rest = false;
16 public $is_cli = false;
17 public $site_url = null;
18 public $files = null;
19 public $tasks = null;
20 public $magicWand = null;
21 private $options = null;
22 private $option_name = 'mwai_options';
23 private $themes_option_name = 'mwai_themes';
24 private $chatbots_option_name = 'mwai_chatbots';
25 private $nonce = null;
26
27 public $chatbot = null;
28 public $discussions = null;
29
30 public function __construct() {
31 Meow_MWAI_Logging::init( 'mwai_options', 'AI Engine' );
32 $this->site_url = get_site_url();
33 $this->is_rest = MeowCommon_Helpers::is_rest();
34 $this->is_cli = defined( 'WP_CLI' );
35 $this->files = new Meow_MWAI_Modules_Files( $this );
36 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
37
38 add_action( 'plugins_loaded', array( $this, 'init' ) );
39 add_action( 'wp_register_script', array( $this, 'register_scripts' ) );
40 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
41 add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
42 }
43
44 #region Init & Scripts
45 function init() {
46 global $mwai;
47 $this->chatbot = null;
48 $this->discussions = null;
49 new Meow_MWAI_Modules_Security( $this );
50
51 // REST API
52 if ( $this->is_rest ) {
53 new Meow_MWAI_Rest( $this );
54 }
55
56 // WP Admin
57 if ( is_admin() ) {
58 new Meow_MWAI_Admin( $this );
59 }
60
61 // GDPR Module
62 if ( $this->get_option( 'chatbot_gdpr_consent' ) ) {
63 new Meow_MWAI_Modules_GDPR( $this );
64 }
65
66 // Suggestions Module
67 if ( $this->get_option( 'module_suggestions' ) && ( is_admin() || $this->is_rest ) ) {
68 $this->magicWand = new Meow_MWAI_Modules_Wand( $this );
69 }
70
71 // Administrator in WP Admin
72 if ( is_admin() && current_user_can( 'manage_options' ) ) {
73 $module_advisor = $this->get_option( 'module_advisor' );
74 if ( $module_advisor ) {
75 new Meow_MWAI_Modules_Advisor( $this );
76 }
77 }
78
79 // Chatbots & Discussions
80 if ( $this->get_option( 'module_chatbots' ) ) {
81 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
82 $this->discussions = new Meow_MWAI_Modules_Discussions();
83 }
84
85 // Advanced Core
86 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
87 new MeowPro_MWAI_Core( $this );
88 }
89
90 // Simple API
91 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
92
93 // MCP
94 if ( $this->get_option( 'module_mcp' ) ) {
95 new Meow_MWAI_Labs_MCP( $this );
96 new Meow_MWAI_Labs_MCP_Core( $this );
97 if ( class_exists( 'MeowPro_MWAI_MCP_Theme' ) ) {
98 new MeowPro_MWAI_MCP_Theme( $this );
99 }
100 }
101 }
102
103 public function register_scripts() {
104 // Register Highlight.js
105 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
106 // Register CSS for the themes
107 $themes = $this->get_themes();
108 foreach ( $themes as $theme ) {
109 if ( $theme['type'] === 'internal' ) {
110 $themeId = $theme['themeId'];
111 $filename = $themeId . '.css';
112 $physical_file = trailingslashit( MWAI_PATH ) . 'themes/' . $filename;
113 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
114 wp_register_style( 'mwai_chatbot_theme_' . $themeId, trailingslashit( MWAI_URL )
115 . 'themes/' . $filename, [], $cache_buster );
116 }
117 }
118 }
119
120 public function enqueue_themes() {
121 // TODO: We should optimize and only load the themes that are used.
122 $themes = $this->get_themes();
123 foreach ( $themes as $theme ) {
124 if ( $theme['type'] === 'internal' ) {
125 $themeId = $theme['themeId'];
126 wp_enqueue_style( "mwai_chatbot_theme_$themeId" );
127 }
128 }
129 }
130
131 #endregion
132
133 #region Roles & Capabilities
134 function can_start_session() {
135 return apply_filters( 'mwai_allow_session', true );
136 }
137
138 function can_access_settings() {
139 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
140 }
141
142 function can_access_features() {
143 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
144 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
145 }
146
147 function can_access_public_api( $feature, $extra ) {
148 $logged_in = is_user_logged_in();
149 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
150 }
151 #endregion
152
153 #region AI-Related Helpers
154 function run_query( $query, $streamCallback = null, $markdown = false ) {
155
156 // Allow to modify the query before it is sent.
157 // Embedding and Feedback queries are not allowed to be modified.
158 if ( !( $query instanceof Meow_MWAI_Query_Embed ) && !( $query instanceof Meow_MWAI_Query_Feedback ) ) {
159 $query = apply_filters( 'mwai_ai_query', $query );
160 }
161
162 // Let's check the default environment and model.
163 $engine = $this->validate_env_model( $query );
164
165 // Let's run the query.
166 $reply = $engine->run( $query, $streamCallback );
167
168 // Let's allow to modify the reply before it is sent.
169 if ( $markdown ) {
170 if ( $query instanceof Meow_MWAI_Query_Image ) {
171 $reply->result = "";
172 foreach ( $reply->results as $result ) {
173 $reply->result .= "![Image]($result)\n";
174 }
175 }
176 }
177
178 return $reply;
179 }
180
181 public function validate_env_model( $query ) {
182 $envId = !empty( $query->envId ) ? $query->envId : null;
183 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
184 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
185 if ( $query instanceof Meow_MWAI_Query_Text ) {
186 $this->set_default_env_and_model( $query, 'ai_default_env', 'ai_default_model' );
187 }
188 if ( $query instanceof Meow_MWAI_Query_Embed ) {
189 $this->set_default_env_and_model( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
190 }
191 else if ( $query instanceof Meow_MWAI_Query_Image ) {
192 $this->set_default_env_and_model( $query, 'ai_images_default_env', 'ai_images_default_model' );
193 }
194 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
195 $this->set_default_env_and_model( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
196 }
197 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
198 }
199 return $engine;
200 }
201
202 private function set_default_env_and_model( $query, $envOption, $modelOption ) {
203 $defaultEnv = $this->get_option( $envOption );
204 $defaultModel = $this->get_option( $modelOption );
205 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
206 throw new Exception( 'AI Engine: The default environment and model are not set.' );
207 }
208 $query->set_env_id( $defaultEnv );
209 $query->set_model( $defaultModel );
210 }
211 #endregion
212
213 #region Text-Related Helpers
214
215 // Clean the text perfectly, resolve shortcodes, etc, etc.
216 function clean_text( $rawText = "" ) {
217 $text = html_entity_decode( $rawText );
218 $text = wp_strip_all_tags( $text );
219 $text = preg_replace( '/[\r\n]+/', "\n", $text );
220 $text = preg_replace( '/\n+/', "\n", $text );
221 $text = preg_replace( '/\t+/', "\t", $text );
222 return $text . " ";
223 }
224
225 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
226 function clean_sentences( $text, $maxLength = null ) {
227 // Step 1: Identify URLs and replace them with a placeholder.
228 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
229 preg_match_all($urlPattern, $text, $urls);
230 $urlPlaceholders = array();
231 foreach ($urls[0] as $index => $url) {
232 $placeholder = "{urlPlaceholder" . $index . "}";
233 $text = str_replace($url, $placeholder, $text);
234 $urlPlaceholders[$placeholder] = $url;
235 }
236
237 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
238 $sentences = preg_split('/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
239 $hashes = array();
240 $uniqueSentences = array();
241 $total = 0;
242
243 foreach ( $sentences as $sentence ) {
244 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
245 $hash = md5( $sentence );
246 if ( !in_array( $hash, $hashes ) ) {
247 $length = mb_strlen( $sentence, 'UTF-8' );
248 if ( $total + $length > $maxLength ) {
249 continue;
250 }
251 $hashes[] = $hash;
252 $uniqueSentences[] = $sentence;
253 $total += $length;
254 }
255 }
256
257 $freshText = implode( " ", $uniqueSentences );
258
259 // Step 3: Restore URLs in the final text.
260 foreach ($urlPlaceholders as $placeholder => $url) {
261 $freshText = str_replace($placeholder, $url, $freshText);
262 }
263
264 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
265 return $freshText;
266 }
267
268 function get_post_content( $postId ) {
269 $post = get_post( $postId );
270 if ( !$post ) {
271 return false;
272 }
273 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
274 $pattern = '/\[mwai_.*?\]/';
275 $text = preg_replace( $pattern, '', $text );
276 if ( $this->get_option( 'resolve_shortcodes' ) ) {
277 $text = apply_filters( 'the_content', $text );
278 }
279 else {
280 $pattern = "/\[[^\]]+\]/";
281 $text = preg_replace( $pattern, '', $text );
282 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
283 $text = preg_replace( $pattern, '', $text );
284 }
285 $text = $this->clean_text( $text );
286 $text = $this->clean_sentences( $text );
287 $text = apply_filters( 'mwai_post_content', $text, $postId );
288 return $text;
289 }
290
291 function markdown_to_html( $content ) {
292 $Parsedown = new Parsedown();
293 $content = $Parsedown->text( $content );
294 return $content;
295 }
296
297 function get_post_language( $postId ) {
298 $locale = get_locale();
299 $code = strtolower( substr( $locale, 0, 2 ) );
300 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
301 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
302 if ( !empty( $lang ) ) {
303 $locale = $lang['locale'];
304 $humanLanguage = $lang['display_name'];
305 }
306 return strtolower( "$locale ($humanLanguage)" );
307 }
308
309 function do_placeholders( $text ) {
310 $defaultPlaceholders = [];
311 $dataPlaceholders = $this->get_user_data();
312 if ( !empty( $dataPlaceholders ) ) {
313 $defaultPlaceholders = array_merge( $defaultPlaceholders, $dataPlaceholders );
314 }
315 $placeholders = apply_filters( 'mwai_placeholders', $defaultPlaceholders );
316 foreach ( $placeholders as $key => $value ) {
317 $text = str_replace( '{' . $key . '}', $value, $text );
318 }
319 return $text;
320 }
321 #endregion
322
323 #region Image-Related Helpers
324 static function is_image( $file ) {
325 $mimeType = Meow_MWAI_Core::get_mime_type( $file );
326 if ( strpos( $mimeType, 'image' ) !== false ) {
327 return true;
328 }
329 return false;
330 }
331
332 static function get_image_resolution( $url ) {
333 if ( empty( $url ) ) {
334 return null;
335 }
336 $headers = get_headers( $url, 1 );
337 if ( strpos( $headers[0], '200' ) === false ) {
338 return null;
339 }
340 $image_info = getimagesize( $url );
341 if ( $image_info === false ) {
342 return null;
343 }
344 return [
345 'width' => $image_info[0],
346 'height' => $image_info[1]
347 ];
348 }
349
350 static function get_mime_type( $file ) {
351 $mimeType = null;
352
353 // Let's try to use mime_content_type if the function exists
354 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
355 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
356 $mimeType = mime_content_type( $file );
357 }
358
359 // Otherwise, let's check the file extension (which can actually also be an URL)
360 if ( !$mimeType ) {
361 $extension = pathinfo( $file, PATHINFO_EXTENSION );
362 $extension = strtolower( $extension );
363 $mimeTypes = [
364 'jpg' => 'image/jpeg',
365 'jpeg' => 'image/jpeg',
366 'png' => 'image/png',
367 'gif' => 'image/gif',
368 'webp' => 'image/webp',
369 'bmp' => 'image/bmp',
370 'tiff' => 'image/tiff',
371 'tif' => 'image/tiff',
372 'svg' => 'image/svg+xml',
373 'ico' => 'image/x-icon',
374 'pdf' => 'application/pdf',
375 ];
376 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
377 }
378
379 return $mimeType;
380 }
381
382 function download_image( $url ) {
383 $args = array( 'timeout' => 60, );
384 $response = wp_safe_remote_get( $url, $args );
385 if ( is_wp_error( $response ) ) {
386 throw new Exception( $response->get_error_message() );
387 }
388 $output = wp_remote_retrieve_body( $response );
389 if ( is_wp_error( $output ) ) {
390 throw new Exception( $output->get_error_message() );
391 }
392 return $output;
393 }
394
395 /**
396 * Add an image from a URL to the Media Library.
397 * @param string $url The URL of the image to be downloaded.
398 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
399 * @param string $title The title of the image.
400 * @param string $description The description of the image.
401 * @param string $caption The caption of the image.
402 * @param string $alt The alt text of the image.
403 * @return int The attachment ID of the image.
404 */
405 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null ) {
406 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
407 $url_filename = $path_parts['basename'];
408 $file_type = wp_check_filetype( $url_filename, null );
409 $allowed_types = get_allowed_mime_types();
410 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
411 throw new Exception( 'Invalid file type from URL.' );
412 }
413
414 // Initial extension from URL file name
415 $extension = $file_type['ext'];
416
417 if ( !empty( $filename ) ) {
418 $custom_file_type = wp_check_filetype( $filename, null );
419 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
420 throw new Exception( 'Invalid custom file type.' );
421 }
422 // Use the extension from the custom filename if valid
423 $extension = $custom_file_type['ext'];
424 }
425
426 $image_data = $this->download_image( $url );
427 if ( !$image_data ) {
428 throw new Exception( 'Could not download the image.' );
429 }
430 $upload_dir = wp_upload_dir();
431
432 // Filename handling including 'generated_' prefix scenario
433 if ( empty( $filename ) ) {
434 $filename = sanitize_file_name( $url_filename );
435 if ( empty( $extension ) ) { // This condition might now be redundant
436 $extension = $file_type['ext'];
437 }
438 // Filename length check and prepend if conditions met
439 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
440 $filename = $this->get_random_id( 16 ) . '.' . $extension;
441 }
442 if ( strpos( $filename, '.' ) === false ) {
443 $filename .= '.' . $extension;
444 }
445 }
446
447 // Directory and file path handling
448 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
449 $file = $upload_dir['path'] . '/' . $filename;
450 }
451 else {
452 $file = $upload_dir['basedir'] . '/' . $filename;
453 }
454
455 // Ensure file name uniqueness in the directory
456 $i = 1;
457 $parts = pathinfo( $file );
458 while ( file_exists( $file ) ) {
459 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
460 $i++;
461 }
462
463 // Writing the file to disk
464 file_put_contents( $file, $image_data );
465
466 // Attachment and metadata handling in WP
467 $attachment = [
468 'post_mime_type' => $file_type['type'],
469 'post_title' => $title ?? '',
470 'post_content' => $description ?? '',
471 'post_excerpt' => $caption ?? '',
472 'post_status' => 'inherit'
473 ];
474 $attachmentId = wp_insert_attachment( $attachment, $file );
475 require_once( ABSPATH . 'wp-admin/includes/image.php' );
476 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
477 wp_update_attachment_metadata( $attachmentId, $attachment_data );
478 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
479
480 // Attach the image to a post if needed
481 if ( $attachedPost ) {
482 wp_update_post( [ 'ID' => $attachmentId, 'post_parent' => $attachedPost ] );
483 }
484
485 return $attachmentId;
486 }
487 #endregion
488
489 #region Context-Related Helpers
490 function retrieve_context( $params, $query ) {
491 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
492 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
493 $context = apply_filters( 'mwai_context_search', [], $query, [
494 'embeddingsEnvId' => $embeddingsEnvId
495 ]);
496 if ( empty( $context ) ) {
497 return null;
498 }
499 else if ( !isset( $context['content'] ) ) {
500 Meow_MWAI_Logging::warn( "A context without content was returned." );
501 return null;
502 }
503 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
504 $context['length'] = strlen( $context['content'] );
505 return $context;
506 }
507 #endregion
508
509 #region Users/Sessions Helpers
510
511 function get_nonce( $force = false ) {
512 if ( !$force && !is_user_logged_in() ) {
513 return null;
514 }
515 if ( isset( $this->nonce ) ) {
516 return $this->nonce;
517 }
518 $this->nonce = wp_create_nonce( 'wp_rest' );
519 return $this->nonce;
520 }
521
522 // This is a bit hacky, but chatId needs to be retrieved or generated.
523 // Maybe we can clean this up later.
524 function fix_chat_id( $query, $params ) {
525 if ( isset( $query->chatId ) && $query->chatId !== 'N/A' ) {
526 return $query->chatId;
527 }
528 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : $query->session;
529 if ( $chatId === 'N/A' ) {
530 $chatId = $this->get_random_id( 8 );
531 }
532 $query->set_chat_id( $chatId );
533 return $chatId;
534 }
535
536 function get_session_id() {
537 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
538 return $_COOKIE['mwai_session_id'];
539 }
540 return "N/A";
541 }
542
543 // Get the UserID from the data, or from the current user
544 function get_user_id( $data = null ) {
545 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
546 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
547 $admin = get_users( [ 'role' => 'administrator' ] );
548 if ( !empty( $admin ) ) {
549 return $admin[0]->ID;
550 }
551 }
552 if ( isset( $data ) && isset( $data['userId'] ) ) {
553 return (int)$data['userId'];
554 }
555 if ( is_user_logged_in() ) {
556 $current_user = wp_get_current_user();
557 if ( $current_user->ID > 0 ) {
558 return $current_user->ID;
559 }
560 }
561 return null;
562 }
563
564 function get_admin_user() {
565 $admin = get_users( [ 'role' => 'administrator' ] );
566 if ( !empty( $admin ) ) {
567 return $admin[0];
568 }
569 return null;
570 }
571
572 function get_user_data() {
573 $user = wp_get_current_user();
574 if ( empty( $user ) || empty( $user->ID ) ) {
575 return null;
576 }
577 $placeholders = array(
578 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
579 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
580 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
581 $user->data->user_login : null,
582 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
583 $user->data->display_name : null,
584 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
585 );
586 return $placeholders;
587 }
588
589 function get_ip_address( $force = false ) {
590 $ip = '127.0.0.1';
591 $headers = [
592 'HTTP_TRUE_CLIENT_IP',
593 'HTTP_CF_CONNECTING_IP',
594 'HTTP_X_REAL_IP',
595 'HTTP_CLIENT_IP',
596 'HTTP_X_FORWARDED_FOR',
597 'HTTP_X_FORWARDED',
598 'HTTP_X_CLUSTER_CLIENT_IP',
599 'HTTP_FORWARDED_FOR',
600 'HTTP_FORWARDED',
601 'REMOTE_ADDR',
602 ];
603
604 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
605 $ip = ( string )$params[ 'ip' ];
606 }
607 else {
608 foreach ( $headers as $header ) {
609 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
610 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
611 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
612 break;
613 }
614 }
615 }
616
617 $ip = filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
618
619 // If privacy_first is enabled, we hash the IP address.
620 if ( !$force && $this->get_option( 'privacy_first' ) && !empty( $ip ) ) {
621 $hash = hash( 'sha256', $ip, true ); // binary output
622 $ip = substr( rtrim( strtr( base64_encode( $hash ), '+/', '-_'), '=' ), 0, 12 );
623 }
624
625 return $ip;
626 }
627
628 #endregion
629
630 #region Sanitization
631 function sanitize_sort( &$sort, $default_accessor = 'created', $default_order = 'DESC',
632 $allowed_columns = array( 'created', 'updated', 'name', 'id', 'time', 'units', 'price' )) {
633
634 // Ensure $sort is an array
635 if ( !is_array( $sort ) ) {
636 $sort = [ "accessor" => $default_accessor, "by" => $default_order ];
637 }
638 // Extract and sanitize the accessor
639 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
640 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
641 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
642 $sort_accessor = $default_accessor;
643 }
644 // Extract and sanitize the sort order
645 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
646 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
647 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
648 $sort_by = $default_order;
649 }
650 // Update the sort array with sanitized values
651 $sort['accessor'] = $sort_accessor;
652 $sort['by'] = $sort_by;
653 }
654 #endregion
655
656 #region Other Helpers
657 function safe_strlen( $string, $encoding = 'UTF-8' ) {
658 if ( function_exists( 'mb_strlen' ) ) {
659 return mb_strlen( $string, $encoding );
660 }
661 else {
662 // Fallback implementation for environments without mbstring extension
663 return preg_match_all( '/./u', $string, $matches );
664 }
665 }
666
667 public function check_rest_nonce( $request ) {
668 $nonce = $request->get_header( 'X-WP-Nonce' );
669 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
670 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
671 }
672
673 function get_random_id( $length = 8, $excludeIds = [] ) {
674 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
675 $charactersLength = strlen( $characters );
676 $randomId = '';
677 for ( $i = 0; $i < $length; $i++ ) {
678 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
679 }
680 if ( in_array( $randomId, $excludeIds ) ) {
681 return $this->get_random_id( $length, $excludeIds );
682 }
683 return $randomId;
684 }
685
686 function is_url( $url ) {
687 return strpos( $url, 'http' ) === 0 ? true : false;
688 }
689
690 function get_post_types() {
691 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
692 $post_types = array();
693 $types = get_post_types( [], 'objects' );
694
695 // Let's get the Post Types that are enabled for Embeddings Sync
696 $embeddingsSettings = $this->get_option( 'embeddings' );
697 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
698
699 foreach ( $types as $type ) {
700 $forced = in_array( $type->name, $syncPostTypes );
701 // Should not be excluded.
702 if ( !$forced && in_array( $type->name, $excluded ) ) {
703 continue;
704 }
705 // Should be public.
706 if ( !$forced && !$type->public ) {
707 continue;
708 }
709 $post_types[] = array(
710 'name' => $type->labels->name,
711 'type' => $type->name,
712 );
713 }
714
715 // Let's get the Post Types that are enabled for Embeddings Sync
716 $embeddingsSettings = $this->get_option( 'embeddings' );
717 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
718
719 return $post_types;
720 }
721
722 function get_post( $post ) {
723 if ( is_numeric( $post ) ) {
724 $post = get_post( $post );
725 }
726 if ( is_object( $post ) ) {
727 $post = (array)$post;
728 }
729 if ( !is_array( $post ) ) {
730 return null;
731 }
732 $language = $this->get_post_language( $post['ID'] );
733 $content = $this->get_post_content( $post['ID'] );
734 $title = $post['post_title'];
735 $excerpt = $post['post_excerpt'];
736 $url = get_permalink( $post['ID'] );
737 $checksum = wp_hash( $content . $title . $url );
738 return [
739 'postId' => (int)$post['ID'],
740 'title' => $title,
741 'content' => $content,
742 'excerpt' => $excerpt,
743 'url' => $url,
744 'language' => $language ?? 'english',
745 'checksum' => $checksum,
746 ];
747 }
748 #endregion
749
750 #region Usage & Costs
751
752 // Quick and dirty token estimation
753 // Let's keep this synchronized with Helpers in JS
754 static function estimate_tokens( ...$args ): int {
755 $text = "";
756 foreach ( $args as $arg ) {
757 if ( is_array( $arg ) ) {
758 foreach ( $arg as $message ) {
759 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
760 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
761 }
762 }
763 else if ( is_string( $arg ) ) {
764 $text .= $arg;
765 }
766 }
767 $averageTokenLength = 4;
768 $words = preg_split( '/\s+/', trim( $text ) );
769 $tokenCount = 0;
770 foreach ( $words as $word ) {
771 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
772 }
773 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
774 }
775
776 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
777 if ( !is_numeric( $in_tokens ) ) {
778 throw new Exception( 'AI Engine: in_tokens must be a number.' );
779 }
780 if ( !is_numeric( $out_tokens ) ) {
781 $out_tokens = 0;
782 }
783 if ( !$model ) {
784 throw new Exception( 'AI Engine: model is required.' );
785 }
786 $usage = $this->get_option( 'ai_models_usage' );
787 $month = date( 'Y-m' );
788 if ( !isset( $usage[$month] ) ) {
789 $usage[$month] = array();
790 }
791 if ( !isset( $usage[$month][$model] ) ) {
792 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
793 }
794 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
795 $usage[$month][$model]['completion_tokens'] += $out_tokens;
796 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
797 $this->update_option( 'ai_models_usage', $usage );
798 $usageInfo = [
799 'prompt_tokens' => $in_tokens,
800 'completion_tokens' => $out_tokens,
801 'total_tokens' => $in_tokens + $out_tokens,
802 ];
803 if ( $returned_price !== null ) {
804 $usageInfo['price'] = $returned_price;
805 }
806 return $usageInfo;
807 }
808
809 public function record_audio_usage( $model, $seconds ) {
810 if ( !is_numeric( $seconds ) ) {
811 throw new Exception( 'AI Engine: seconds must be a number.' );
812 }
813 if ( !$model ) {
814 throw new Exception( 'AI Engine: model is required.' );
815 }
816 $usage = $this->get_option( 'ai_models_usage' );
817 $month = date( 'Y-m' );
818 if ( !isset( $usage[$month] ) ) {
819 $usage[$month] = array();
820 }
821 if ( !isset( $usage[$month][$model] ) ) {
822 $usage[$month][$model] = array( 'seconds' => 0 );
823 }
824 $usage[$month][$model]['seconds'] += $seconds;
825 $this->update_option( 'ai_models_usage', $usage );
826 return [ 'seconds' => $seconds ];
827 }
828
829 public function record_images_usage( $model, $resolution, $images ) {
830 if ( !$model || !$resolution || !$images ) {
831 throw new Exception( 'Missing parameters for record_image_usage.' );
832 }
833 $usage = $this->get_option( 'ai_models_usage' );
834 $month = date( 'Y-m' );
835 if ( !isset( $usage[$month] ) ) {
836 $usage[$month] = array();
837 }
838 if ( !isset( $usage[$month][$model] ) ) {
839 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
840 }
841 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
842 $usage[$month][$model]['resolution'][$resolution] = 0;
843 }
844 $usage[$month][$model]['resolution'][$resolution] += $images;
845 $usage[$month][$model]['images'] += $images;
846 $this->update_option( 'ai_models_usage', $usage );
847 return [ 'resolution' => $resolution, 'images' => $images ];
848 }
849
850 #endregion
851
852 #region Streaming
853 public function stream_push( $data, $query = null ) {
854 $data = apply_filters( 'mwai_stream_push', $data, $query );
855 $out = "data: " . json_encode( $data );
856 echo $out;
857 echo "\n\n";
858 if ( ob_get_level() > 0 ) {
859 ob_end_flush();
860 }
861 flush();
862 }
863 #endregion
864
865 #region Options
866 function get_themes() {
867 $themes = get_option( $this->themes_option_name, [] );
868 $themes = empty( $themes ) ? [] : $themes;
869
870 $internalThemes = [
871 'chatgpt' => [
872 'type' => 'internal','name' => 'ChatGPT', 'themeId' => 'chatgpt',
873 'settings' => [], 'style' => ""
874 ],
875 'messages' => [
876 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
877 'settings' => [], 'style' => ""
878 ],
879 'timeless' => [
880 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
881 'settings' => [], 'style' => ""
882 ],
883 ];
884 $customThemes = [];
885 foreach ( $themes as $theme ) {
886 if ( isset( $internalThemes[$theme['themeId']] ) ) {
887 $internalThemes[$theme['themeId']] = $theme;
888 continue;
889 }
890 $customThemes[] = $theme;
891 }
892 return array_merge(array_values($internalThemes), $customThemes);
893 }
894
895 function update_themes( $themes ) {
896 update_option( $this->themes_option_name, $themes );
897 return $themes;
898 }
899
900 function get_chatbots() {
901 $chatbots = get_option( $this->chatbots_option_name, [] );
902 $hasChanges = false;
903 if ( empty( $chatbots ) ) {
904 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
905 }
906 $hasDefault = false;
907 foreach ( $chatbots as &$chatbot ) {
908 if ( $chatbot['botId'] === 'default' ) {
909 $hasDefault = true;
910 }
911 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
912 // Use default value if not set.
913 if ( !isset( $chatbot[$key] ) ) {
914 $chatbot[$key] = $value;
915 }
916 }
917
918 /*
919 This is the best section to rename fields.
920 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).
921 */
922
923 // if ( isset( $chatbot['context'] ) ) {
924 // $chatbot['instructions'] = $chatbot['context'];
925 // unset( $chatbot['context'] );
926 // $hasChanges = true;
927 // }
928 }
929 if ( !$hasDefault ) {
930 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
931 array_unshift( $chatbots, $defaultBot );
932 $hasChanges = true;
933 }
934 if ( $hasChanges ) {
935 update_option( $this->chatbots_option_name, $chatbots );
936 }
937 return $chatbots;
938 }
939
940 function get_chatbot( $botId ) {
941 $chatbots = $this->get_chatbots();
942 foreach ( $chatbots as $chatbot ) {
943 if ( $chatbot['botId'] === (string)$botId ) {
944 return $chatbot;
945 }
946 }
947 return null;
948 }
949
950 function get_embeddings_env( $envId ) {
951 $envs = $this->get_option( 'embeddings_envs' );
952 foreach ( $envs as $env ) {
953 if ( $env['id'] === $envId ) {
954 return $env;
955 }
956 }
957 return null;
958 }
959
960 function get_ai_env( $envId ) {
961 $envs = $this->get_option( 'ai_envs' );
962 foreach ( $envs as $env ) {
963 if ( $env['id'] === $envId ) {
964 return $env;
965 }
966 }
967 return null;
968 }
969
970 function get_assistant( $envId, $assistantId ) {
971 $env = $this->get_ai_env( $envId );
972 if ( !$env ) {
973 return null;
974 }
975 $assistants = $env['assistants'];
976 foreach ( $assistants as $assistant ) {
977 if ( $assistant['id'] === $assistantId ) {
978 return $assistant;
979 }
980 }
981 return null;
982 }
983
984 function get_theme( $themeId ) {
985 $themes = $this->get_themes();
986 foreach ( $themes as $theme ) {
987 if ( $theme['themeId'] === $themeId ) {
988 return $theme;
989 }
990 }
991 return null;
992 }
993
994 function update_chatbots( $chatbots ) {
995 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
996 // TODO: I think some HTML fields are missing, guestName, maybe others.
997 $htmlFields = [ 'instructions', 'textCompliance', 'aiName', 'userName', 'startSentence' ];
998 $keepLineReturnsFields = [ 'instructions' ];
999 $whiteSpacedFields = [ 'context' ];
1000 foreach ( $chatbots as &$chatbot ) {
1001 foreach ( $chatbot as $key => &$value ) {
1002 if ( in_array( $key, $deprecatedFields ) ) {
1003 unset( $chatbot[$key] );
1004 continue;
1005 }
1006 if ( in_array( $key, $htmlFields ) ) {
1007 $value = wp_kses_post( $value );
1008 }
1009 else if ( in_array( $key, $whiteSpacedFields ) ) {
1010 $value = sanitize_textarea_field( $value );
1011 }
1012 else if ( $key === 'functions' ) {
1013 $functions = [];
1014 foreach ( $value as $function ) {
1015 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
1016 $functions[] = [
1017 'id' => sanitize_text_field( $function['id'] ),
1018 'type' => sanitize_text_field( $function['type'] ),
1019 ];
1020 }
1021 }
1022 $value = $functions;
1023 }
1024 else {
1025 if ( in_array( $key, $keepLineReturnsFields ) ) {
1026 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
1027 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
1028 }
1029 $value = sanitize_text_field( $value );
1030 if ( in_array( $key, $keepLineReturnsFields ) ) {
1031 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1032 }
1033 }
1034 }
1035 }
1036 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1037 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1038 $chatbots = get_option( $this->chatbots_option_name, [] );
1039 return $chatbots;
1040 }
1041 return $chatbots;
1042 }
1043
1044 function populate_dynamic_options( $options ) {
1045 // Languages
1046 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1047
1048 // Consolidate the Engines and their Models
1049 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1050 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1051 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1052 foreach ( $options['ai_engines'] as &$engine ) {
1053 if ( $engine['type'] === 'openai' ) {
1054 $engine['models'] = apply_filters( 'mwai_openai_models',
1055 Meow_MWAI_Engines_OpenAI::get_models_static()
1056 );
1057 }
1058 else if ( $engine['type'] === 'anthropic' ) {
1059 $engine['models'] = apply_filters( 'mwai_anthropic_models',
1060 Meow_MWAI_Engines_Anthropic::get_models_static()
1061 );
1062 }
1063 else if ( $engine['type'] === 'perplexity' ) {
1064 $engine['models'] = apply_filters( 'mwai_perplexity_models',
1065 Meow_MWAI_Engines_Perplexity::get_models_static()
1066 );
1067 }
1068 else {
1069 $engine['models'] = [];
1070 foreach ( $options['ai_models'] as $model ) {
1071 if ( $model['type'] === $engine['type'] ) {
1072 $engine['models'][] = $model;
1073 }
1074 }
1075 }
1076 }
1077
1078 // Functions via Code Engine (or custom code)
1079 $json = [];
1080 $functions = apply_filters( 'mwai_functions_list', [] );
1081 foreach ( $functions as $function ) {
1082 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1083 }
1084 $options['functions'] = $json;
1085
1086 // Addons
1087 $options['addons'] = apply_filters( 'mwai_addons', [
1088 [
1089 "slug" => "mwai-notifications",
1090 "name" => "Notifications",
1091 "description" => "Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.",
1092 "install_url" => "https://meowapps.com/products/mwai-notifications/",
1093 "settings_url" => null,
1094 "stars" => 4,
1095 "enabled" => false
1096 ],
1097 [
1098 "slug" => "mwai-ollama",
1099 "name" => "Ollama",
1100 "description" => "Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.",
1101 "install_url" => "https://meowapps.com/products/mwai-ollama/",
1102 "settings_url" => null,
1103 "stars" => 3,
1104 "enabled" => false
1105 ],
1106 [
1107 "slug" => "mwai-deepseek",
1108 "name" => "DeepSeek",
1109 "description" => "Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.",
1110 "install_url" => "https://meowapps.com/products/deepseek/",
1111 "settings_url" => null,
1112 "stars" => 3,
1113 "enabled" => false
1114 ],
1115 [
1116 "slug" => "mwai-websearch",
1117 "name" => "Web Search",
1118 "description" => "Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.",
1119 "install_url" => "https://meowapps.com/products/mwai-websearch/",
1120 "settings_url" => null,
1121 "stars" => 5,
1122 "enabled" => false
1123 ],
1124 [
1125 "slug" => "mwai-better-links",
1126 "name" => "Better Links",
1127 "description" => "Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.",
1128 "install_url" => "https://meowapps.com/products/mwai-better-links/",
1129 "settings_url" => null,
1130 "stars" => 3,
1131 "enabled" => false
1132 ],
1133 [
1134 "slug" => "mwai-woo-basics",
1135 "name" => "Woo Basics",
1136 "description" => "Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.",
1137 "install_url" => "https://meowapps.com/products/mwai-woo-basics/",
1138 "settings_url" => null,
1139 "stars" => 2,
1140 "enabled" => false
1141 ],
1142 [
1143 "slug" => "mwai-quick-actions",
1144 "name" => "Quick Actions",
1145 "description" => "Enable dynamic quick actions at chat start or during events, helping users find what they need faster.",
1146 "install_url" => "https://meowapps.com/products/mwai-quick-actions/",
1147 "settings_url" => null,
1148 "stars" => 3,
1149 "enabled" => false
1150 ],
1151 [
1152 "slug" => "mwai-content-parser",
1153 "name" => "Content Parser",
1154 "description" => "Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.",
1155 "install_url" => "https://meowapps.com/products/mwai-content-parser/",
1156 "settings_url" => null,
1157 "stars" => 2,
1158 "enabled" => false
1159 ],
1160 [
1161 "slug" => "mwai-visitor-form",
1162 "name" => "Visitor Form",
1163 "description" => "Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.",
1164 "install_url" => "https://meowapps.com/products/mwai-visitor-form/",
1165 "settings_url" => null,
1166 "stars" => 2,
1167 "enabled" => false
1168 ],
1169 [
1170 "slug" => "mwai-dynamic-keys",
1171 "name" => "Dynamic Keys",
1172 "description" => "Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.",
1173 "install_url" => "https://meowapps.com/products/mwai-dynamic-keys/",
1174 "settings_url" => null,
1175 "stars" => 1,
1176 "enabled" => false
1177 ],
1178 ] );
1179
1180 return $options;
1181 }
1182
1183 function get_all_options( $force = false, $sanitize = false ) {
1184 if ( $force || is_null( $this->options ) ) {
1185 $options = get_option( $this->option_name, [] );
1186 $init_mode = empty( $options );
1187 foreach ( MWAI_OPTIONS as $key => $value ) {
1188 if ( !isset( $options[$key] ) ) {
1189 $options[$key] = $value;
1190 }
1191 }
1192 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1193 $options['default_limits'] = MWAI_LIMITS;
1194 if ( $sanitize || $init_mode ) {
1195 $options = $this->sanitize_options( $options );
1196 }
1197 $this->options = $options;
1198 }
1199 $options = $this->populate_dynamic_options( $this->options );
1200 return $options;
1201 }
1202
1203 // Sanitize options when we update the plugin or perform some updates
1204 // if we change the structure of the options.
1205 function sanitize_options( $options ) {
1206 $needs_update = false;
1207
1208 // Removing old options of options renaming should be done here, as it was done before.
1209 // Check version 2.6.8 for an example.
1210
1211 // Avoid the logs_path to be a PHP file.
1212 if ( isset( $options['logs_path'] ) ) {
1213 $logs_path = $options['logs_path'];
1214 if ( substr( $logs_path, -4 ) !== '.log' ) {
1215 $options['logs_path'] = '';
1216 $needs_update = true;
1217 }
1218 }
1219
1220 // The IDs for the embeddings environments are generated here.
1221 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1222 $embeddings_default_exists = false;
1223 if ( isset( $options['embeddings_envs'] ) ) {
1224 foreach ( $options['embeddings_envs'] as &$env ) {
1225 if ( !isset( $env['id'] ) ) {
1226 $env['id'] = $this->get_random_id();
1227 $needs_update = true;
1228 }
1229 if ( $env['id'] === $options['embeddings_default_env'] ) {
1230 $embeddings_default_exists = true;
1231 }
1232 }
1233 }
1234 if ( !$embeddings_default_exists ) {
1235 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1236 $needs_update = true;
1237 }
1238
1239 // The IDs for the AI environments are generated here.
1240 $allEnvIds = [];
1241 $ai_default_exists = false;
1242 if ( isset( $options['ai_envs'] ) ) {
1243 foreach ( $options['ai_envs'] as &$env ) {
1244 if ( !isset( $env['id'] ) ) {
1245 $env['id'] = $this->get_random_id();
1246 $needs_update = true;
1247 }
1248 if ( $env['id'] === $options['ai_default_env'] ) {
1249 $ai_default_exists = true;
1250 }
1251 $allEnvIds[] = $env['id'];
1252 }
1253 }
1254 if ( !$ai_default_exists ) {
1255 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1256 $needs_update = true;
1257 }
1258
1259 // All the models with an envId that does not exist anymore are removed.
1260 if ( isset( $options['ai_models'] ) ) {
1261 $options['ai_models'] = array_values( array_filter( $options['ai_models'],
1262 function( $model ) use ( $allEnvIds, &$needs_update ) {
1263 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1264 $needs_update = true;
1265 return false;
1266 }
1267 return true;
1268 }
1269 ) );
1270 }
1271
1272 if ( $needs_update ) {
1273 ksort( $options );
1274 update_option( $this->option_name, $options, false );
1275 }
1276
1277 return $options;
1278 }
1279
1280 function update_options( $options ) {
1281 if ( !update_option( $this->option_name, $options, false ) ) {
1282 return false;
1283 }
1284 $options = $this->get_all_options( true, true );
1285 return $options;
1286 }
1287
1288 function update_option( $option, $value ) {
1289 $options = $this->get_all_options( true );
1290 $options[$option] = $value;
1291 return $this->update_options( $options );
1292 }
1293
1294 function get_option( $option, $default = null ) {
1295 $options = $this->get_all_options();
1296 return $options[$option] ?? $default;
1297 }
1298
1299 function update_ai_env( $env_id, $option, $value ) {
1300 $options = $this->get_all_options( true );
1301 foreach ( $options['ai_envs'] as &$env ) {
1302 if ( $env['id'] === $env_id ) {
1303 $env[$option] = $value;
1304 break;
1305 }
1306 }
1307 return $this->update_options( $options );
1308 }
1309
1310 function get_engine_models( $engineType ) {
1311 $engines = $this->get_option( 'ai_engines' );
1312 foreach ( $engines as $engine ) {
1313 if ( $engine['type'] === $engineType ) {
1314 return isset( $engine['models'] ) ? $engine['models'] : [];
1315 }
1316 }
1317 return [];
1318 }
1319
1320 function reset_options() {
1321 delete_option( $this->themes_option_name );
1322 delete_option( $this->chatbots_option_name );
1323 delete_option( $this->option_name );
1324 return $this->get_all_options( true );
1325 }
1326 #endregion
1327 }
1328
1329 ?>