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