PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.1
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.1
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 / modules / chatbot.php
ai-engine / classes / modules Last commit date
advisor.php 2 years ago chatbot.php 1 year ago discussions.php 1 year ago files.php 2 years ago security.php 2 years ago tasks.php 2 years ago wand.php 2 years ago
chatbot.php
664 lines
1 <?php
2
3 // Params for the chatbot (front and server)
4 define( 'MWAI_CHATBOT_FRONT_PARAMS', [ 'id', 'customId',
5 'aiName', 'userName', 'guestName',
6 'aiAvatar', 'userAvatar', 'guestAvatar',
7 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl',
8 'textSend', 'textClear', 'imageUpload', 'fileSearch',
9 'textInputPlaceholder', 'textInputMaxLength', 'textCompliance', 'startSentence', 'localMemory',
10 'themeId', 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', 'iconBubble',
11 'fullscreen', 'copyButton'
12 ] );
13 define( 'MWAI_CHATBOT_SERVER_PARAMS', [ 'id', 'envId', 'scope', 'mode', 'contentAware', 'context',
14 'embeddingsEnvId', 'embeddingsIndex', 'embeddingsNamespace', 'assistantId', 'instructions',
15 'model', 'temperature', 'maxTokens', 'contextMaxLength', 'maxResults', 'apiKey', 'functions'
16 ] );
17
18 // Params for the discussions (front and server)
19 define( 'MWAI_DISCUSSIONS_FRONT_PARAMS', [ 'themeId', 'textNewChat' ] );
20 define( 'MWAI_DISCUSSIONS_SERVER_PARAMS', [ 'customId' ] );
21
22 class Meow_MWAI_Modules_Chatbot {
23 private $core = null;
24 private $namespace = 'mwai-ui/v1';
25 private $siteWideChatId = null;
26
27 public function __construct() {
28 global $mwai_core;
29 $this->core = $mwai_core;
30 $this->siteWideChatId = $this->core->get_option( 'botId' );
31
32 add_shortcode( 'mwai_chatbot', array( $this, 'chat_shortcode' ) );
33 add_shortcode( 'mwai_chatbot_v2', array( $this, 'old_chat_shortcode' ) );
34 add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
35 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
36 add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
37 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
38 add_shortcode( 'mwai_discussions', [ $this, 'chatbot_discussions' ] );
39 }
40 }
41
42 public function register_scripts() {
43 // Load JS
44 $physical_file = trailingslashit( MWAI_PATH ) . 'app/chatbot.js';
45 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
46 wp_register_script( 'mwai_chatbot', trailingslashit( MWAI_URL )
47 . 'app/chatbot.js', [ 'wp-element' ], $cache_buster, false );
48
49 // Actual loading of the scripts
50 $hasSiteWideChat = $this->siteWideChatId && $this->siteWideChatId !== 'none';
51 if ( is_admin() || $hasSiteWideChat ) {
52 $this->enqueue_scripts();
53 if ( $hasSiteWideChat ) {
54 // Chatbot Injection
55 add_action( 'wp_footer', array( $this, 'inject_chat' ) );
56 }
57 }
58 }
59
60 public function enqueue_scripts() {
61 wp_enqueue_script( "mwai_chatbot" );
62 if ( $this->core->get_option( 'syntax_highlight' ) ) {
63 wp_enqueue_script( "mwai_highlight" );
64 }
65 $this->core->enqueue_themes();
66 }
67
68 public function rest_api_init() {
69 register_rest_route( $this->namespace, '/chats/submit', array(
70 'methods' => 'POST',
71 'callback' => [ $this, 'rest_chat' ],
72 'permission_callback' => array( $this->core, 'check_rest_nonce' )
73 ) );
74 }
75
76 public function basics_security_check( $botId, $customId, $newMessage, $newFileId ) {
77 if ( !$botId && !$customId ) {
78 $this->core->log( "⚠️ The query was rejected - no botId nor id was specified.");
79 return false;
80 }
81
82 if ( $newFileId ) {
83 return true;
84 }
85
86 $length = strlen( empty( $newMessage ) ? "" : $newMessage );
87 if ( $length < 1 || $length > ( 4096 * 16 ) ) {
88 $this->core->log( "⚠️ The query was rejected - message was too short or too long.");
89 return false;
90 }
91 return true;
92 }
93
94 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage ) {
95 $filterParams = [
96 'botId' => $botId,
97 'reply' => $reply,
98 'images' => $images,
99 'newMessage' => $newMessage,
100 'newFileId' => $newFileId,
101 'params' => $params,
102 'usage' => $usage,
103 ];
104 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
105 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
106 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
107 $actions = $this->sanitize_actions( $actions );
108 $blocks = $this->sanitize_blocks( $blocks );
109 $shortcuts = $this->sanitize_shortcuts( $shortcuts );
110 return [
111 'success' => true,
112 'reply' => $reply,
113 'images' => $images,
114 'actions' => $actions,
115 'shortcuts' => $shortcuts,
116 'blocks' => $blocks,
117 'usage' => $usage
118 ];
119 }
120
121 public function rest_chat( $request ) {
122 $params = $request->get_json_params();
123 $botId = $params['botId'] ?? null;
124 $customId = $params['customId'] ?? null;
125 $stream = $params['stream'] ?? false;
126 $newMessage = trim( $params['newMessage'] ?? '' );
127 $newFileId = $params['newFileId'] ?? null;
128
129 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId )) {
130 return new WP_REST_Response( [
131 'success' => false,
132 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
133 ], 403 );
134 }
135
136 try {
137 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream );
138 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
139 $data['reply'], $data['images'], $data['actions'], $data['usage'] );
140 return new WP_REST_Response( $final_res, 200 );
141 }
142 catch ( Exception $e ) {
143 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
144 return new WP_REST_Response( [
145 'success' => false,
146 'message' => $message
147 ], 500 );
148 }
149 }
150
151 private function sanitize_items( $items, $supported_types, $type_name ) {
152 if ( empty( $items ) ) {
153 return $items;
154 }
155 $sanitized_items = [];
156 foreach ( $items as $item ) {
157 if ( isset( $supported_types[$item['type']] ) ) {
158 $is_valid = true;
159 foreach ( $supported_types[$item['type']] as $param ) {
160 if ( empty( $item['data'][$param] ) ) {
161 $is_valid = false;
162 $this->core->log( "⚠️ Missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
163 break;
164 }
165 }
166 if ( $is_valid ) {
167 $sanitized_items[] = $item;
168 }
169 }
170 else {
171 $this->core->log( "⚠️ Unsupported {$type_name} type: {$item['type']}." );
172 }
173 }
174 return $sanitized_items;
175 }
176
177 public function sanitize_actions( $actions ) {
178 $supported_action_types = [
179 'function' => ['name', 'args'],
180 'javascript' => ['snippet'],
181 ];
182 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
183 }
184
185 public function sanitize_blocks( $blocks ) {
186 $supported_block_types = [
187 'content' => ['html'],
188 ];
189 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
190 }
191
192 public function sanitize_shortcuts( $shortcuts ) {
193 $supported_shortcut_types = [
194 'message' => ['label', 'message'],
195 ];
196 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
197 }
198
199 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false ) {
200 try {
201 $chatbot = null;
202 $customId = $params['customId'] ?? null;
203
204 // Custom Chatbot
205 if ( $customId ) {
206 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
207 }
208 // Registered Chatbot
209 if ( !$chatbot && $botId ) {
210 $chatbot = $this->core->get_chatbot( $botId );
211 }
212
213 if ( !$chatbot ) {
214 $this->core->log( "⚠️ No chatbot was found for this query.");
215 throw new Exception( 'Sorry, your query has been rejected.' );
216 }
217
218 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
219 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int)$textInputMaxLength ) {
220 error_log( "AI Engine: The query was rejected - message was too long.");
221 throw new Exception( 'Sorry, your query has been rejected.' );
222 }
223
224 // Create QueryText
225 $context = null;
226 $mode = $chatbot['mode'] ?? 'chat';
227
228 if ( $mode === 'images' ) {
229 $query = new Meow_MWAI_Query_Image( $newMessage );
230
231 // Handle Params
232 $newParams = [];
233 foreach ( $chatbot as $key => $value ) {
234 $newParams[$key] = $value;
235 }
236 foreach ( $params as $key => $value ) {
237 $newParams[$key] = $value;
238 }
239 $params = apply_filters( 'mwai_chatbot_params', $newParams );
240 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
241 $query->inject_params( $params );
242 }
243 else {
244 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
245 new Meow_MWAI_Query_Text( $newMessage, 1024 );
246 $streamCallback = null;
247
248 // Handle Params
249 $newParams = [];
250 foreach ( $chatbot as $key => $value ) {
251 $newParams[$key] = $value;
252 }
253 foreach ( $params as $key => $value ) {
254 $newParams[$key] = $value;
255 }
256 $params = apply_filters( 'mwai_chatbot_params', $newParams );
257 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
258 $query->inject_params( $params );
259
260 $storeId = null;
261 if ( $mode === 'assistant' ) {
262 $chatId = $params['chatId'] ?? null;
263 if ( !empty( $chatId ) ) {
264 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
265 if ( isset( $discussion['storeId'] ) ) {
266 $storeId = $discussion['storeId'];
267 $query->setStoreId( $storeId );
268 }
269 }
270 }
271
272 // Support for Uploaded Image
273 if ( !empty( $newFileId ) ) {
274
275 // Get extension and mime type
276 $isImage = $this->core->files->is_image( $newFileId );
277
278 if ( $mode === 'assistant' && !$isImage ) {
279 $url = $this->core->files->get_path( $newFileId );
280 $data = $this->core->files->get_data( $newFileId );
281 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
282 $filename = basename( $url );
283
284 // Upload the file
285 $file = $openai->upload_file( $filename, $data, 'assistants' );
286
287 // Create a store
288 if ( empty( $storeId ) ) {
289 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
290 if ( !empty( $query->chatId ) ) {
291 $chatbotName .= "_" . $query->chatId;
292 }
293 $metadata = [];
294 if ( !empty( $chatbot['assistantId'] ) ) {
295 $metadata['assistantId'] = $chatbot['assistantId'];
296 }
297 if ( !empty( $query->chatId ) ) {
298 $metadata['chatId'] = $query->chatId;
299 }
300 $expiry = $this->core->get_option( 'image_expires' );
301 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
302 $query->setStoreId( $storeId );
303 }
304
305 // Add the file to the store
306 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
307
308 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
309 $openAiRefId = $file['id'];
310 $internalFileId = $this->core->files->get_id_from_refId( $newFileId );
311 $this->core->files->update_refId( $internalFileId, $openAiRefId );
312 $this->core->files->update_envId( $internalFileId, $query->envId );
313 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
314 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
315 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
316 $newFileId = $openAiRefId;
317 $scope = $params['fileSearch'];
318 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
319 $id = $this->core->files->get_id_from_refId( $newFileId );
320 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
321 }
322 }
323 else {
324 $url = $this->core->files->get_url( $newFileId );
325 $mimeType = $this->core->files->get_mime_type( $newFileId );
326 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
327 $fileId = $this->core->files->get_id_from_refId( $newFileId );
328 $this->core->files->update_envId( $fileId, $query->envId );
329 $this->core->files->update_purpose( $fileId, 'vision' );
330 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
331 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
332 }
333 }
334
335 // Takeover
336 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
337 if ( !empty( $takeoverAnswer ) ) {
338 return [
339 'reply' => $takeoverAnswer,
340 'images' => null,
341 'usage' => null
342 ];
343 }
344
345 // Moderation
346 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
347 $this->core->get_option( 'shortcode_chat_moderation' );
348 if ( $moderationEnabled ) {
349 global $mwai;
350 $isFlagged = $mwai->moderationCheck( $query->get_message() );
351 if ( $isFlagged ) {
352 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
353 }
354 }
355
356 // Awareness & Embeddings
357 $context = $this->core->retrieve_context( $params, $query );
358 if ( !empty( $context ) ) {
359 $query->set_context( $context['content'] );
360 }
361
362 // Function Aware
363 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
364 }
365
366 // Process Query
367 if ( $stream ) {
368 $streamCallback = function( $reply ) {
369 $raw = $reply;
370 $this->core->stream_push( [ 'type' => 'live', 'data' => $raw ] );
371 // if ( ob_get_level() > 0 ) {
372 // ob_flush();
373 // }
374 // flush();
375 };
376 header( 'Cache-Control: no-cache' );
377 header( 'Content-Type: text/event-stream' );
378 // This is useful to disable buffering in nginx through headers.
379 header( 'X-Accel-Buffering: no' );
380 ob_implicit_flush( true );
381 ob_end_flush();
382 }
383
384 $reply = $this->core->run_query( $query, $streamCallback, true );
385 $rawText = $reply->result;
386 $extra = [];
387 if ( $context ) {
388 $extra = [ 'embeddings' => $context['embeddings'] ];
389 }
390 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
391
392 $actions = [];
393 if ( $reply->needClientActions ) {
394 foreach ( $reply->needClientActions as $action ) {
395 $actions[] = [
396 'type' => 'function',
397 'data' => [
398 'name' => $action['function']->name,
399 'args' => $action['arguments']
400 ]
401 ];
402 }
403 }
404
405 $restRes = [
406 'reply' => $rawText,
407 'chatId' => $this->core->fix_chat_id( $query, $params ),
408 'images' => $reply->get_type() === 'images' ? $reply->results : null,
409 'actions' => $actions,
410 'usage' => $reply->usage
411 ];
412
413 // Process Reply
414 if ( $stream ) {
415 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
416 $restRes['reply'], $restRes['images'], $restRes['actions'], $restRes['usage'] );
417 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ] );
418 die();
419 }
420 else {
421 return $restRes;
422 }
423
424 }
425 catch ( Exception $e ) {
426 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
427 if ( $stream ) {
428 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ] );
429 die();
430 }
431 else {
432 throw $e;
433 }
434 }
435 }
436
437 public function inject_chat() {
438 $params = $this->core->get_chatbot( $this->siteWideChatId );
439 $clean_params = [];
440 if ( !empty( $params ) ) {
441 $clean_params['window'] = true;
442 $clean_params['id'] = $this->siteWideChatId;
443 echo $this->chat_shortcode( $clean_params );
444 }
445 return null;
446 }
447
448 public function build_front_params( $botId, $customId ) {
449 $frontSystem = [
450 'botId' => $customId ? null : $botId,
451 'customId' => $customId,
452 'userData' => $this->core->get_user_data(),
453 'sessionId' => $this->core->get_session_id(),
454 'restNonce' => $this->core->get_nonce(),
455 'contextId' => get_the_ID(),
456 'pluginUrl' => MWAI_URL,
457 'restUrl' => untrailingslashit( get_rest_url() ),
458 'stream' => $this->core->get_option( 'ai_streaming' ),
459 'debugMode' => $this->core->get_option('module_devtools') && $this->core->get_option( 'debug_mode' ),
460 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
461 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
462 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
463 'virtual_keyboard_fix' => $this->core->get_option( 'virtual_keyboard_fix' )
464 ];
465 return $frontSystem;
466 }
467
468 public function resolveBotInfo( &$atts )
469 {
470 $chatbot = null;
471 $botId = $atts['id'] ?? null;
472 $customId = $atts['custom_id'] ?? null;
473 if (!$botId && !$customId) {
474 $botId = "default";
475 }
476 if ( $botId ) {
477 $chatbot = $this->core->get_chatbot( $botId );
478 if (!$chatbot) {
479 $botId = $botId ?: 'N/A';
480 return [
481 'error' => "AI Engine: Chatbot '{$botId}' not found. If you meant to set an ID for your custom chatbot, please use 'custom_id' instead of 'id'.",
482 ];
483 }
484 }
485 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
486 if ( !empty( $customId ) ) {
487 $botId = null;
488 }
489 unset( $atts['id'] );
490 return [
491 'chatbot' => $chatbot,
492 'botId' => $botId,
493 'customId' => $customId,
494 ];
495 }
496
497 // TODO: After January 2025, remove this.
498 public function old_chat_shortcode( $atts ) {
499 error_log( "AI Engine: The shortcode 'mwai_chatbot_v2' is deprecated. Please use 'mwai_chatbot' instead." );
500 return $this->chat_shortcode( $atts );
501 }
502
503 public function chat_shortcode( $atts ) {
504 $atts = empty( $atts ) ? [] : $atts;
505
506 // Let the user override the chatbot params
507 $atts = apply_filters( 'mwai_chatbot_params', $atts );
508
509 // Resolve the bot info
510 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
511 if ( isset( $resolvedBot['error'] ) ) {
512 return $resolvedBot['error'];
513 }
514 $chatbot = $resolvedBot['chatbot'];
515 $botId = $resolvedBot['botId'];
516 $customId = $resolvedBot['customId'];
517
518 // Rename the keys of the atts into camelCase to match the internal params system.
519 $atts = array_map( function( $key, $value ) {
520 $key = str_replace( '_', ' ', $key );
521 $key = ucwords( $key );
522 $key = str_replace( ' ', '', $key );
523 $key = lcfirst( $key );
524 return [ $key => $value ];
525 }, array_keys( $atts ), $atts );
526 $atts = array_merge( ...$atts );
527
528 $frontParams = [];
529 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
530 if ( isset( $atts[$param] ) ) {
531 if ( $param === 'localMemory' ) {
532 $frontParams[$param] = $atts[$param] === 'true';
533 }
534 else {
535 $frontParams[$param] = $atts[$param];
536 }
537 }
538 else if ( isset( $chatbot[$param] ) ) {
539 $frontParams[$param] = $chatbot[$param];
540 }
541 }
542
543 // Server Params
544 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
545 // we are using the default or a specific chatbot.
546 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
547 $serverParams = [];
548 if ( $hasServerOverrides ) {
549 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
550 if ( isset( $atts[$param] ) ) {
551 $serverParams[$param] = $atts[$param];
552 }
553 else {
554 $serverParams[$param] = $chatbot[$param] ?? null;
555 }
556 }
557 }
558
559 // Front Params
560 $frontSystem = $this->build_front_params( $botId, $customId );
561
562 // Clean Params
563 $frontParams = $this->clean_params( $frontParams );
564 $frontSystem = $this->clean_params( $frontSystem );
565 $serverParams = $this->clean_params( $serverParams );
566
567 // Server-side: Keep the System Params
568 if ( $hasServerOverrides ) {
569 if ( empty( $customId ) ) {
570 $customId = md5( json_encode( $serverParams ) );
571 $frontSystem['customId'] = $customId;
572 }
573 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
574 }
575
576 // Client-side: Prepare JSON for Front Params and System Params
577 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
578 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
579 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
580 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
581 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
582
583 $this->enqueue_scripts();
584 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
585 }
586
587 function chatbot_discussions( $atts ) {
588 $atts = empty($atts) ? [] : $atts;
589
590 // Resolve the bot info
591 $resolvedBot = $this->resolveBotInfo( $atts );
592 if ( isset( $resolvedBot['error'] ) ) {
593 return $resolvedBot['error'];
594 }
595 $chatbot = $resolvedBot['chatbot'];
596 $botId = $resolvedBot['botId'];
597 $customId = $resolvedBot['customId'];
598
599 // Rename the keys of the atts into camelCase to match the internal params system.
600 $atts = array_map( function( $key, $value ) {
601 $key = str_replace( '_', ' ', $key );
602 $key = ucwords( $key );
603 $key = str_replace( ' ', '', $key );
604 $key = lcfirst( $key );
605 return [ $key => $value ];
606 }, array_keys( $atts ), $atts );
607 $atts = array_merge( ...$atts );
608
609 // Front Params
610 $frontParams = [];
611 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
612 if ( isset( $atts[$param] ) ) {
613 $frontParams[$param] = $atts[$param];
614 }
615 else if ( isset( $chatbot[$param] ) ) {
616 $frontParams[$param] = $chatbot[$param];
617 }
618 }
619
620 // Server Params
621 $serverParams = [];
622 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
623 if ( isset( $atts[$param] ) ) {
624 $serverParams[$param] = $atts[$param];
625 }
626 }
627
628 // Front System
629 $frontSystem = $this->build_front_params( $botId, $customId );
630
631 // Clean Params
632 $frontParams = $this->clean_params( $frontParams );
633 $frontSystem = $this->clean_params( $frontSystem );
634 $serverParams = $this->clean_params( $serverParams );
635
636 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
637 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
638 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
639 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
640
641 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
642 }
643
644 function clean_params( &$params ) {
645 foreach ( $params as $param => $value ) {
646 if ( $param === 'restNonce' ) {
647 continue;
648 }
649 if ( empty( $value ) || is_array( $value ) ) {
650 continue;
651 }
652 $lowerCaseValue = strtolower( $value );
653 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
654 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
655 }
656 else if ( is_numeric( $value ) ) {
657 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
658 }
659 }
660 return $params;
661 }
662
663 }
664