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