PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.9
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / 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 1 year ago tasks.php 2 years ago wand.php 1 year ago
chatbot.php
687 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
14 define( 'MWAI_CHATBOT_SERVER_PARAMS', [ 'id', 'envId', 'scope', 'mode', 'contentAware', 'context',
15 'embeddingsEnvId', 'embeddingsIndex', 'embeddingsNamespace', 'assistantId', 'instructions',
16 'model', 'temperature', 'maxTokens', 'contextMaxLength', 'maxResults', 'apiKey', 'functions'
17 ] );
18
19 // Params for the discussions (front and server)
20 define( 'MWAI_DISCUSSIONS_FRONT_PARAMS', [ 'themeId', 'textNewChat' ] );
21 define( 'MWAI_DISCUSSIONS_SERVER_PARAMS', [ 'customId' ] );
22
23 class Meow_MWAI_Modules_Chatbot {
24 private $core = null;
25 private $namespace = 'mwai-ui/v1';
26 private $siteWideChatId = null;
27
28 public function __construct() {
29 global $mwai_core;
30 $this->core = $mwai_core;
31 $this->siteWideChatId = $this->core->get_option( 'botId' );
32
33 add_shortcode( 'mwai_chatbot', array( $this, 'chat_shortcode' ) );
34 add_shortcode( 'mwai_chatbot_v2', array( $this, 'old_chat_shortcode' ) );
35 add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
36 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
37 add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
38 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
39 add_shortcode( 'mwai_discussions', [ $this, 'chatbot_discussions' ] );
40 }
41 }
42
43 public function register_scripts() {
44 // Load JS
45 $physical_file = trailingslashit( MWAI_PATH ) . 'app/chatbot.js';
46 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
47 wp_register_script( 'mwai_chatbot', trailingslashit( MWAI_URL )
48 . 'app/chatbot.js', [ 'wp-element' ], $cache_buster, false );
49
50 // Actual loading of the scripts
51 $hasSiteWideChat = $this->siteWideChatId && $this->siteWideChatId !== 'none';
52 if ( is_admin() || $hasSiteWideChat ) {
53 $this->enqueue_scripts();
54 if ( $hasSiteWideChat ) {
55 // Chatbot Injection
56 add_action( 'wp_footer', array( $this, 'inject_chat' ) );
57 }
58 }
59 }
60
61 public function enqueue_scripts() {
62 wp_enqueue_script( "mwai_chatbot" );
63 if ( $this->core->get_option( 'syntax_highlight' ) ) {
64 wp_enqueue_script( "mwai_highlight" );
65 }
66 $this->core->enqueue_themes();
67 }
68
69 public function rest_api_init() {
70 register_rest_route( $this->namespace, '/chats/submit', array(
71 'methods' => 'POST',
72 'callback' => [ $this, 'rest_chat' ],
73 'permission_callback' => array( $this->core, 'check_rest_nonce' )
74 ) );
75 }
76
77 public function basics_security_check( $botId, $customId, $newMessage, $newFileId ) {
78 if ( !$botId && !$customId ) {
79 Meow_MWAI_Logging::warn( "The query was rejected - no botId nor id was specified." );
80 return false;
81 }
82
83 if ( $newFileId ) {
84 return true;
85 }
86
87 $length = strlen( empty( $newMessage ) ? "" : $newMessage );
88 if ( $length < 1 || $length > ( 4096 * 16 ) ) {
89 Meow_MWAI_Logging::warn( "The query was rejected - message was too short or too long." );
90 return false;
91 }
92 return true;
93 }
94
95 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage ) {
96 $filterParams = [
97 'step' => 'reply',
98 'botId' => $botId,
99 'reply' => $reply,
100 'images' => $images,
101 'newMessage' => $newMessage,
102 'newFileId' => $newFileId,
103 'params' => $params,
104 'usage' => $usage,
105 ];
106 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
107 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
108 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
109 $actions = $this->sanitize_actions( $actions );
110 $blocks = $this->sanitize_blocks( $blocks );
111 $shortcuts = $this->sanitize_shortcuts( $shortcuts );
112 return [
113 'success' => true,
114 'reply' => $reply,
115 'images' => $images,
116 'actions' => $actions,
117 'shortcuts' => $shortcuts,
118 'blocks' => $blocks,
119 'usage' => $usage
120 ];
121 }
122
123 public function rest_chat( $request ) {
124 $params = $request->get_json_params();
125 $botId = $params['botId'] ?? null;
126 $customId = $params['customId'] ?? null;
127 $stream = $params['stream'] ?? false;
128 $newMessage = trim( $params['newMessage'] ?? '' );
129 $newFileId = $params['newFileId'] ?? null;
130
131 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId )) {
132 return new WP_REST_Response( [
133 'success' => false,
134 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
135 ], 403 );
136 }
137
138 try {
139 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream );
140 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
141 $data['reply'], $data['images'], $data['actions'], $data['usage'] );
142 return new WP_REST_Response( $final_res, 200 );
143 }
144 catch ( Exception $e ) {
145 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
146 return new WP_REST_Response( [
147 'success' => false,
148 'message' => $message
149 ], 500 );
150 }
151 }
152
153 private function sanitize_items( $items, $supported_types, $type_name ) {
154 if ( empty( $items ) ) {
155 return $items;
156 }
157 $sanitized_items = [];
158 foreach ( $items as $item ) {
159 if ( isset( $supported_types[$item['type']] ) ) {
160 $is_valid = true;
161 foreach ( $supported_types[$item['type']] as $param ) {
162 if ( !isset( $item['data'][$param] ) ) {
163 $is_valid = false;
164 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
165 break;
166 }
167 }
168 if ( $is_valid ) {
169 $sanitized_items[] = $item;
170 }
171 }
172 else {
173 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
174 }
175 }
176 return $sanitized_items;
177 }
178
179 public function sanitize_actions( $actions ) {
180 $supported_action_types = [
181 'function' => ['name', 'args'],
182 'javascript' => ['snippet'],
183 ];
184 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
185 }
186
187 public function sanitize_blocks( $blocks ) {
188 $supported_block_types = [
189 'content' => ['html'],
190 ];
191 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
192 }
193
194 public function sanitize_shortcuts( $shortcuts ) {
195 $supported_shortcut_types = [
196 'message' => ['label', 'message'],
197 ];
198 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
199 }
200
201 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false ) {
202 try {
203 $chatbot = null;
204 $customId = $params['customId'] ?? null;
205
206 // Custom Chatbot
207 if ( $customId ) {
208 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
209 }
210 // Registered Chatbot
211 if ( !$chatbot && $botId ) {
212 $chatbot = $this->core->get_chatbot( $botId );
213 }
214
215 if ( !$chatbot ) {
216 Meow_MWAI_Logging::warn( "The query was rejected - no chatbot was found." );
217 throw new Exception( 'Sorry, your query has been rejected.' );
218 }
219
220 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
221 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int)$textInputMaxLength ) {
222 Meow_MWAI_Logging::warn( "The query was rejected - message was too long." );
223 throw new Exception( 'Sorry, your query has been rejected.' );
224 }
225
226 // Create QueryText
227 $context = null;
228 $mode = $chatbot['mode'] ?? 'chat';
229
230 if ( $mode === 'images' ) {
231 $query = new Meow_MWAI_Query_Image( $newMessage );
232
233 // Handle Params
234 $newParams = [];
235 foreach ( $chatbot as $key => $value ) {
236 $newParams[$key] = $value;
237 }
238 foreach ( $params as $key => $value ) {
239 $newParams[$key] = $value;
240 }
241 $params = apply_filters( 'mwai_chatbot_params', $newParams );
242 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
243 $query->inject_params( $params );
244 }
245 else {
246 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
247 new Meow_MWAI_Query_Text( $newMessage, 1024 );
248 $streamCallback = null;
249
250 // Handle Params
251 $newParams = [];
252 foreach ( $chatbot as $key => $value ) {
253 $newParams[$key] = $value;
254 }
255 foreach ( $params as $key => $value ) {
256 $newParams[$key] = $value;
257 }
258 $params = apply_filters( 'mwai_chatbot_params', $newParams );
259 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
260 $query->inject_params( $params );
261
262 $storeId = null;
263 if ( $mode === 'assistant' ) {
264 $chatId = $params['chatId'] ?? null;
265 if ( !empty( $chatId ) ) {
266 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
267 if ( isset( $discussion['storeId'] ) ) {
268 $storeId = $discussion['storeId'];
269 $query->setStoreId( $storeId );
270 }
271 }
272 }
273
274 // Support for Uploaded Image
275 if ( !empty( $newFileId ) ) {
276
277 // Get extension and mime type
278 $isImage = $this->core->files->is_image( $newFileId );
279
280 if ( $mode === 'assistant' && !$isImage ) {
281 $url = $this->core->files->get_path( $newFileId );
282 $data = $this->core->files->get_data( $newFileId );
283 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
284 $filename = basename( $url );
285
286 // Upload the file
287 $file = $openai->upload_file( $filename, $data, 'assistants' );
288
289 // Create a store
290 if ( empty( $storeId ) ) {
291 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
292 if ( !empty( $query->chatId ) ) {
293 $chatbotName .= "_" . $query->chatId;
294 }
295 $metadata = [];
296 if ( !empty( $chatbot['assistantId'] ) ) {
297 $metadata['assistantId'] = $chatbot['assistantId'];
298 }
299 if ( !empty( $query->chatId ) ) {
300 $metadata['chatId'] = $query->chatId;
301 }
302 $expiry = $this->core->get_option( 'image_expires' );
303 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
304 $query->setStoreId( $storeId );
305 }
306
307 // Add the file to the store
308 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
309
310 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
311 $openAiRefId = $file['id'];
312 $internalFileId = $this->core->files->get_id_from_refId( $newFileId );
313 $this->core->files->update_refId( $internalFileId, $openAiRefId );
314 $this->core->files->update_envId( $internalFileId, $query->envId );
315 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
316 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
317 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
318 $newFileId = $openAiRefId;
319 $scope = $params['fileSearch'];
320 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
321 $id = $this->core->files->get_id_from_refId( $newFileId );
322 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
323 }
324 }
325 else {
326 $url = $this->core->files->get_url( $newFileId );
327 $mimeType = $this->core->files->get_mime_type( $newFileId );
328 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
329 $fileId = $this->core->files->get_id_from_refId( $newFileId );
330 $this->core->files->update_envId( $fileId, $query->envId );
331 $this->core->files->update_purpose( $fileId, 'vision' );
332 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
333 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
334 }
335 }
336
337 // Takeover
338 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
339 if ( !empty( $takeoverAnswer ) ) {
340 return [
341 'reply' => $takeoverAnswer,
342 'images' => null,
343 'usage' => null
344 ];
345 }
346
347 // Moderation
348 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
349 $this->core->get_option( 'shortcode_chat_moderation' );
350 if ( $moderationEnabled ) {
351 global $mwai;
352 $isFlagged = $mwai->moderationCheck( $query->get_message() );
353 if ( $isFlagged ) {
354 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
355 }
356 }
357
358 // Awareness & Embeddings
359 $context = $this->core->retrieve_context( $params, $query );
360 if ( !empty( $context ) ) {
361 $query->set_context( $context['content'] );
362 }
363
364 // Function Aware
365 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
366 }
367
368 // Process Query
369 if ( $stream ) {
370 $streamCallback = function( $reply ) use ( $query ) {
371 $raw = $reply;
372 $this->core->stream_push( [ 'type' => 'live', 'data' => $raw ], $query );
373 // if ( ob_get_level() > 0 ) {
374 // ob_flush();
375 // }
376 // flush();
377 };
378 header( 'Cache-Control: no-cache' );
379 header( 'Content-Type: text/event-stream' );
380 // This is useful to disable buffering in nginx through headers.
381 header( 'X-Accel-Buffering: no' );
382 ob_implicit_flush( true );
383 ob_end_flush();
384 }
385
386 $reply = $this->core->run_query( $query, $streamCallback, true );
387 $rawText = $reply->result;
388 $extra = [];
389 if ( $context ) {
390 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
391 }
392 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
393
394 $actions = [];
395 if ( $reply->needClientActions ) {
396 foreach ( $reply->needClientActions as $action ) {
397 $actions[] = [
398 'type' => 'function',
399 'data' => [
400 'name' => $action['function']->name,
401 'args' => $action['arguments']
402 ]
403 ];
404 }
405 }
406
407 $restRes = [
408 'reply' => $rawText,
409 'chatId' => $this->core->fix_chat_id( $query, $params ),
410 'images' => $reply->get_type() === 'images' ? $reply->results : null,
411 'actions' => $actions,
412 'usage' => $reply->usage
413 ];
414
415 // Process Reply
416 if ( $stream ) {
417 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
418 $restRes['reply'], $restRes['images'], $restRes['actions'], $restRes['usage'] );
419 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
420 die();
421 }
422 else {
423 return $restRes;
424 }
425
426 }
427 catch ( Exception $e ) {
428 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
429 if ( $stream ) {
430 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
431 die();
432 }
433 else {
434 throw $e;
435 }
436 }
437 }
438
439 public function inject_chat() {
440 $params = $this->core->get_chatbot( $this->siteWideChatId );
441 $clean_params = [];
442 if ( !empty( $params ) ) {
443 $clean_params['window'] = true;
444 $clean_params['id'] = $this->siteWideChatId;
445 echo $this->chat_shortcode( $clean_params );
446 }
447 return null;
448 }
449
450 public function build_front_params( $botId, $customId ) {
451 $frontSystem = [
452 'botId' => $customId ? null : $botId,
453 'customId' => $customId,
454 'userData' => $this->core->get_user_data(),
455 'sessionId' => $this->core->get_session_id(),
456 'restNonce' => $this->core->get_nonce(),
457 'contextId' => get_the_ID(),
458 'pluginUrl' => MWAI_URL,
459 'restUrl' => untrailingslashit( get_rest_url() ),
460 'stream' => $this->core->get_option( 'ai_streaming' ),
461 'debugMode' => $this->core->get_option('module_devtools') && $this->core->get_option( 'debug_mode' ),
462 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
463 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
464 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
465 'virtual_keyboard_fix' => $this->core->get_option( 'virtual_keyboard_fix' )
466 ];
467 return $frontSystem;
468 }
469
470 public function resolveBotInfo( &$atts )
471 {
472 $chatbot = null;
473 $botId = $atts['id'] ?? null;
474 $customId = $atts['custom_id'] ?? null;
475 if (!$botId && !$customId) {
476 $botId = "default";
477 }
478 if ( $botId ) {
479 $chatbot = $this->core->get_chatbot( $botId );
480 if (!$chatbot) {
481 $botId = $botId ?: 'N/A';
482 return [
483 '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'.",
484 ];
485 }
486 }
487 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
488 if ( !empty( $customId ) ) {
489 $botId = null;
490 }
491 unset( $atts['id'] );
492 return [
493 'chatbot' => $chatbot,
494 'botId' => $botId,
495 'customId' => $customId,
496 ];
497 }
498
499 // TODO: After January 2025, remove this.
500 public function old_chat_shortcode( $atts ) {
501 Meow_MWAI_Logging::deprecated( "The shortcode 'mwai_chatbot_v2' is deprecated. Please use 'mwai_chatbot' instead." );
502 return $this->chat_shortcode( $atts );
503 }
504
505 public function chat_shortcode( $atts ) {
506 $atts = empty( $atts ) ? [] : $atts;
507
508 // Let the user override the chatbot params
509 $atts = apply_filters( 'mwai_chatbot_params', $atts );
510
511 // Resolve the bot info
512 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
513 if ( isset( $resolvedBot['error'] ) ) {
514 return $resolvedBot['error'];
515 }
516 $chatbot = $resolvedBot['chatbot'];
517 $botId = $resolvedBot['botId'];
518 $customId = $resolvedBot['customId'];
519
520 // Rename the keys of the atts into camelCase to match the internal params system.
521 $atts = array_map( function( $key, $value ) {
522 $key = str_replace( '_', ' ', $key );
523 $key = ucwords( $key );
524 $key = str_replace( ' ', '', $key );
525 $key = lcfirst( $key );
526 return [ $key => $value ];
527 }, array_keys( $atts ), $atts );
528 $atts = array_merge( ...$atts );
529
530 $frontParams = [];
531 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
532 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
533 if ( isset( $atts[$param] ) ) {
534 if ( $param === 'localMemory' ) {
535 $frontParams[$param] = $atts[$param] === 'true';
536 }
537 else {
538 $frontParams[$param] = $atts[$param];
539 }
540 }
541 // If not, let's use the chatbot's default values
542 else if ( isset( $chatbot[$param] ) ) {
543 $frontParams[$param] = $chatbot[$param];
544 }
545
546 // Apply the placeholders
547 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
548 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
549 }
550 }
551
552 // Server Params
553 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
554 // we are using the default or a specific chatbot.
555 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
556 $serverParams = [];
557 if ( $hasServerOverrides ) {
558 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
559 if ( isset( $atts[$param] ) ) {
560 $serverParams[$param] = $atts[$param];
561 }
562 else {
563 $serverParams[$param] = $chatbot[$param] ?? null;
564 }
565 }
566 }
567
568 // Front Params
569 $frontSystem = $this->build_front_params( $botId, $customId );
570
571 // Clean Params
572 $frontParams = $this->clean_params( $frontParams );
573 $frontSystem = $this->clean_params( $frontSystem );
574 $serverParams = $this->clean_params( $serverParams );
575
576 // Server-side: Keep the System Params
577 if ( $hasServerOverrides ) {
578 if ( empty( $customId ) ) {
579 $customId = md5( json_encode( $serverParams ) );
580 $frontSystem['customId'] = $customId;
581 }
582 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
583 }
584
585 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
586 $filterParams = [
587 'step' => 'init',
588 'botId' => $botId,
589 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
590 ];
591 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
592 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
593 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
594 $frontSystem['actions'] = $this->sanitize_actions( $actions );
595 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
596 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
597
598 // Client-side: Prepare JSON for Front Params and System Params
599 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
600 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
601 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
602 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
603 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
604
605 $this->enqueue_scripts();
606
607 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
608 }
609
610 function chatbot_discussions( $atts ) {
611 $atts = empty($atts) ? [] : $atts;
612
613 // Resolve the bot info
614 $resolvedBot = $this->resolveBotInfo( $atts );
615 if ( isset( $resolvedBot['error'] ) ) {
616 return $resolvedBot['error'];
617 }
618 $chatbot = $resolvedBot['chatbot'];
619 $botId = $resolvedBot['botId'];
620 $customId = $resolvedBot['customId'];
621
622 // Rename the keys of the atts into camelCase to match the internal params system.
623 $atts = array_map( function( $key, $value ) {
624 $key = str_replace( '_', ' ', $key );
625 $key = ucwords( $key );
626 $key = str_replace( ' ', '', $key );
627 $key = lcfirst( $key );
628 return [ $key => $value ];
629 }, array_keys( $atts ), $atts );
630 $atts = array_merge( ...$atts );
631
632 // Front Params
633 $frontParams = [];
634 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
635 if ( isset( $atts[$param] ) ) {
636 $frontParams[$param] = $atts[$param];
637 }
638 else if ( isset( $chatbot[$param] ) ) {
639 $frontParams[$param] = $chatbot[$param];
640 }
641 }
642
643 // Server Params
644 $serverParams = [];
645 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
646 if ( isset( $atts[$param] ) ) {
647 $serverParams[$param] = $atts[$param];
648 }
649 }
650
651 // Front System
652 $frontSystem = $this->build_front_params( $botId, $customId );
653
654 // Clean Params
655 $frontParams = $this->clean_params( $frontParams );
656 $frontSystem = $this->clean_params( $frontSystem );
657 $serverParams = $this->clean_params( $serverParams );
658
659 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
660 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
661 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
662 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
663
664 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
665 }
666
667 function clean_params( &$params ) {
668 foreach ( $params as $param => $value ) {
669 if ( $param === 'restNonce' ) {
670 continue;
671 }
672 if ( empty( $value ) || is_array( $value ) ) {
673 continue;
674 }
675 $lowerCaseValue = strtolower( $value );
676 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
677 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
678 }
679 else if ( is_numeric( $value ) ) {
680 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
681 }
682 }
683 return $params;
684 }
685
686 }
687