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