PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.4
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 1 year ago gdpr.php 1 year ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 1 year ago
chatbot.php
911 lines
1 <?php
2
3 // Params for the chatbot (front and server)
4 define( 'MWAI_CHATBOT_FRONT_PARAMS', [ 'id', 'customId', 'aiName', 'userName', 'guestName', 'aiAvatar', 'userAvatar', 'guestAvatar', 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl', 'textSend', 'textClear', 'imageUpload', 'fileUpload', 'fileSearch', 'mode', 'textInputPlaceholder', 'textInputMaxLength', 'textCompliance', 'startSentence', 'localMemory', 'themeId', 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', 'iconBubble', 'fullscreen', 'copyButton', 'headerSubtitle' ] );
5
6 define( 'MWAI_CHATBOT_SERVER_PARAMS', [ 'id', 'envId', 'scope', 'mode', 'contentAware', 'context', 'startSentence', 'embeddingsEnvId', 'embeddingsIndex', 'embeddingsNamespace', 'assistantId', 'instructions', 'resolution', 'voice', 'model', 'temperature', 'maxTokens', 'contextMaxLength', 'maxResults', 'apiKey', 'functions', 'mcpServers', 'tools', 'historyStrategy', 'previousResponseId', 'parentBotId' ] );
7
8 // Params for the discussions (front and server)
9 define( 'MWAI_DISCUSSIONS_FRONT_PARAMS', [ 'themeId', 'textNewChat' ] );
10 define( 'MWAI_DISCUSSIONS_SERVER_PARAMS', [ 'customId' ] );
11
12 class Meow_MWAI_Modules_Chatbot {
13 private $core = null;
14 private $namespace = 'mwai-ui/v1';
15 private $siteWideChatId = null;
16
17 public function __construct() {
18 global $mwai_core;
19 $this->core = $mwai_core;
20 $this->siteWideChatId = $this->core->get_option( 'botId' );
21
22 add_shortcode( 'mwai_chatbot', array( $this, 'chat_shortcode' ) );
23 add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
24 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
25 add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
26 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
27 add_shortcode( 'mwai_discussions', [ $this, 'chatbot_discussions' ] );
28 }
29 }
30
31 public function register_scripts() {
32 // Load JS
33 $physical_file = trailingslashit( MWAI_PATH ) . 'app/chatbot.js';
34 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
35 wp_register_script( 'mwai_chatbot', trailingslashit( MWAI_URL )
36 . 'app/chatbot.js', [ 'wp-element' ], $cache_buster, false );
37
38 // Actual loading of the scripts
39 $hasSiteWideChat = $this->siteWideChatId && $this->siteWideChatId !== 'none';
40 if ( is_admin() || $hasSiteWideChat ) {
41 $themeId = null;
42 if ( $hasSiteWideChat ) {
43 $bot = $this->core->get_chatbot( $this->siteWideChatId );
44 if ( $bot && isset( $bot['themeId'] ) ) {
45 $themeId = $bot['themeId'];
46 }
47 }
48 $this->enqueue_scripts( is_admin() ? null : $themeId );
49 if ( $hasSiteWideChat ) {
50 // Chatbot Injection
51 add_action( 'wp_footer', array( $this, 'inject_chat' ) );
52 }
53 }
54 }
55
56 public function enqueue_scripts( $themeId = null ) {
57 wp_enqueue_script( "mwai_chatbot" );
58 if ( $this->core->get_option( 'syntax_highlight' ) ) {
59 wp_enqueue_script( "mwai_highlight" );
60 }
61 if ( $themeId ) {
62 $this->core->enqueue_theme( $themeId );
63 }
64 else {
65 $this->core->enqueue_themes();
66 }
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 ) {
89 Meow_MWAI_Logging::warn( "The query was rejected - message was too short." );
90 return false;
91 }
92 return true;
93 }
94
95 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage, $responseId = null ) {
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 'messages' => $params['messages'] ?? [],
106 'isNewConversation' => empty( $params['messages'] ) || count( $params['messages'] ) <= 1,
107 ];
108 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
109 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
110 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
111 $actions = $this->sanitize_actions( $actions );
112 $blocks = $this->sanitize_blocks( $blocks );
113 $shortcuts = $this->sanitize_shortcuts( $shortcuts );
114 $result = [
115 'success' => true,
116 'reply' => $reply,
117 'images' => $images,
118 'actions' => $actions,
119 'shortcuts' => $shortcuts,
120 'blocks' => $blocks,
121 'usage' => $usage
122 ];
123
124 // Add response ID if available
125 if ( !empty( $responseId ) ) {
126 $result['responseId'] = $responseId;
127 }
128
129 return $result;
130 }
131
132 public function rest_chat( $request ) {
133 $params = $request->get_json_params();
134 $botId = $params['botId'] ?? null;
135 $customId = $params['customId'] ?? null;
136 $stream = $params['stream'] ?? false;
137 $newMessage = trim( $params['newMessage'] ?? '' );
138 $newFileId = $params['newFileId'] ?? null;
139
140
141 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId )) {
142 return new WP_REST_Response( [
143 'success' => false,
144 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
145 ], 403 );
146 }
147
148 try {
149 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream );
150 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
151 $data['reply'], $data['images'], $data['actions'], $data['usage'],
152 $data['responseId'] ?? null );
153 return new WP_REST_Response( $final_res, 200 );
154 }
155 catch ( Exception $e ) {
156 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
157 return new WP_REST_Response( [
158 'success' => false,
159 'message' => $message
160 ], 500 );
161 }
162 }
163
164 private function sanitize_items( $items, $supported_types, $type_name ) {
165 if ( empty( $items ) ) {
166 return $items;
167 }
168 $sanitized_items = [];
169 foreach ( $items as $item ) {
170 if ( isset( $supported_types[$item['type']] ) ) {
171 $is_valid = true;
172 foreach ( $supported_types[$item['type']] as $param ) {
173 if ( !isset( $item['data'][$param] ) ) {
174 $is_valid = false;
175 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
176 break;
177 }
178 }
179 if ( $is_valid ) {
180 $sanitized_items[] = $item;
181 }
182 }
183 else {
184 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
185 }
186 }
187 return $sanitized_items;
188 }
189
190 public function sanitize_actions( $actions ) {
191 $supported_action_types = [
192 'function' => ['name', 'args'],
193 'javascript' => ['snippet'],
194 ];
195 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
196 }
197
198 public function sanitize_blocks( $blocks ) {
199 $supported_block_types = [
200 'content' => ['html'],
201 ];
202 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
203 }
204
205 public function sanitize_shortcuts( $shortcuts ) {
206 $supported_shortcut_types = [
207 'message' => ['label', 'message'],
208 'callback' => ['label', 'onClick'],
209 ];
210 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
211 }
212
213 #region Messages Integrity Check
214
215 function messages_integrity_diff( $messages1, $messages2 ) {
216 // Collect messages with role not 'user' from messages1
217 $messagesList1 = array();
218 foreach( $messages1 as $msg ) {
219 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
220 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
221 if( $role && $role != 'user' ) {
222 $messageData = array( 'role' => $role, 'content' => $content );
223 $messagesList1[] = $messageData;
224 }
225 }
226
227 // Collect messages with role not 'user' from messages2
228 $messagesList2 = array();
229 foreach( $messages2 as $msg ) {
230 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
231 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
232 if( $role && $role != 'user' ) {
233 $messageData = array( 'role' => $role, 'content' => $content );
234 $messagesList2[] = $messageData;
235 }
236 }
237
238 // Count occurrences of each message in messagesList1
239 $counts1 = array();
240 foreach( $messagesList1 as $msg ) {
241 $key = serialize( $msg );
242 if( isset( $counts1[ $key ] ) ) {
243 $counts1[ $key ]++;
244 } else {
245 $counts1[ $key ] = 1;
246 }
247 }
248
249 // Count occurrences of each message in messagesList2
250 $counts2 = array();
251 foreach( $messagesList2 as $msg ) {
252 $key = serialize( $msg );
253 if( isset( $counts2[ $key ] ) ) {
254 $counts2[ $key ]++;
255 } else {
256 $counts2[ $key ] = 1;
257 }
258 }
259
260 // Compare counts to find unmatched messages
261 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
262
263 $diffs = array();
264 foreach( $all_keys as $key ) {
265 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
266 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
267 if( $count1 != $count2 ) {
268 $message = unserialize( $key );
269 $diffs[] = array(
270 'message' => $message,
271 'count_in_messages1' => $count1,
272 'count_in_messages2' => $count2
273 );
274 }
275 }
276
277 return $diffs;
278 }
279
280 private function calculate_messages_checksum( $messages ) {
281 $messages_to_hash = [];
282 foreach ( $messages as $msg ) {
283 $role = is_array( $msg ) ? ($msg['role'] ?? '') : (is_object( $msg ) ? ($msg->role ?? '') : '');
284 $content = is_array( $msg ) ? ($msg['content'] ?? '') : (is_object( $msg ) ? ($msg->content ?? '') : '');
285 if ( in_array( $role, ['assistant', 'system'] ) ) {
286 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
287 }
288 }
289 return md5( json_encode( $messages_to_hash ) );
290 }
291
292 #endregion
293
294 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false ) {
295 try {
296 $chatbot = null;
297 $customId = $params['customId'] ?? null;
298
299 // Custom Chatbot
300 if ( $customId ) {
301 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
302 }
303 // Registered Chatbot
304 if ( !$chatbot && $botId ) {
305 $chatbot = $this->core->get_chatbot( $botId );
306 }
307
308 if ( !$chatbot ) {
309 Meow_MWAI_Logging::warn( "The query was rejected - no chatbot was found." );
310 throw new Exception( 'Sorry, your query has been rejected.' );
311 }
312
313 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
314 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int)$textInputMaxLength ) {
315 Meow_MWAI_Logging::warn( "The query was rejected - message was too long." );
316 throw new Exception( 'Sorry, your query has been rejected.' );
317 }
318
319 // We need to check the integrity of the messages sent by the client.
320 // This is important to ensure that the messages are not tampered with.
321
322 // Messages Integrity Check with Checksums
323 $chatId = $params['chatId'] ?? 'default';
324 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
325 $stored_checksum = get_transient( $checksum_key );
326 $client_messages = $params['messages'] ?? [];
327 $client_checksum = $this->calculate_messages_checksum( $client_messages );
328 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
329 Meow_MWAI_Logging::warn( "Integrity Check: Messages integrity check failed. Assistant or system messages sent by the client do not match stored messages. Please enable the Discussions module for better logs." );
330 }
331
332 // Messages Integrity Check with Discussions
333 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
334 $discussion = $this->core->discussions->get_discussion( $botId ? $botId: $customId, $params['chatId'] );
335 if ( $discussion ) {
336 $messages = $discussion['messages'];
337 $diffs = $this->messages_integrity_diff( $messages, $params['messages'] );
338 if ( count( $diffs ) > 0 ) {
339 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
340 }
341
342 // Load previousResponseId from discussion extra data if not provided by client
343 if ( empty( $params['previousResponseId'] ) && !empty( $discussion['extra'] ) ) {
344 $extra = json_decode( $discussion['extra'], true );
345 if ( !empty( $extra['previousResponseId'] ) ) {
346 // Check if response is not older than 30 days
347 $responseDate = !empty( $extra['previousResponseDate'] ) ? strtotime( $extra['previousResponseDate'] ) : 0;
348 $thirtyDaysAgo = time() - (30 * 24 * 60 * 60);
349
350 if ( $responseDate > $thirtyDaysAgo ) {
351 $params['previousResponseId'] = $extra['previousResponseId'];
352 }
353 }
354 }
355 }
356 else {
357 // No discussion yet? We still need to check the startSentence.
358 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
359 $messages = [];
360 if ( !empty( $startSentence ) ) {
361 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
362 }
363 $diffs = $this->messages_integrity_diff( $messages, $params['messages'] );
364 if ( count( $diffs ) > 0 ) {
365 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion do not match the ones sent by the client: " . json_encode( $diffs ) );
366 }
367 }
368 }
369
370 // Create QueryText
371 $context = null;
372 $streamCallback = null;
373 $mode = $chatbot['mode'] ?? 'chat';
374
375 if ( $mode === 'images' ) {
376 // If there's an uploaded file, use EditImage query instead
377 if ( !empty( $newFileId ) ) {
378 $query = new Meow_MWAI_Query_EditImage( $newMessage );
379
380 // Handle the uploaded image
381 $url = $this->core->files->get_url( $newFileId );
382 $mimeType = $this->core->files->get_mime_type( $newFileId );
383 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
384
385 if ( $isIMG ) {
386 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
387 $fileId = $this->core->files->get_id_from_refId( $newFileId );
388 $this->core->files->update_purpose( $fileId, 'vision' );
389 }
390 }
391 else {
392 $query = new Meow_MWAI_Query_Image( $newMessage );
393 }
394
395 // Handle Params
396 $newParams = [];
397 foreach ( $chatbot as $key => $value ) {
398 $newParams[$key] = $value;
399 }
400 foreach ( $params as $key => $value ) {
401 $newParams[$key] = $value;
402 }
403 $params = apply_filters( 'mwai_chatbot_params', $newParams );
404 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
405
406
407 $query->inject_params( $params );
408 }
409 else {
410 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
411 new Meow_MWAI_Query_Text( $newMessage, 1024 );
412
413 // Handle Params
414 $newParams = [];
415 foreach ( $chatbot as $key => $value ) {
416 $newParams[$key] = $value;
417 }
418 foreach ( $params as $key => $value ) {
419 $newParams[$key] = $value;
420 }
421 $params = apply_filters( 'mwai_chatbot_params', $newParams );
422 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
423
424
425 $query->inject_params( $params );
426
427 $storeId = null;
428 if ( $mode === 'assistant' ) {
429 $chatId = $params['chatId'] ?? null;
430 if ( !empty( $chatId ) ) {
431 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
432 if ( isset( $discussion['storeId'] ) ) {
433 $storeId = $discussion['storeId'];
434 $query->setStoreId( $storeId );
435 }
436 }
437 }
438
439 // Support for Uploaded Image
440 if ( !empty( $newFileId ) ) {
441
442 // Get extension and mime type
443 $isImage = $this->core->files->is_image( $newFileId );
444
445 if ( $mode === 'assistant' && !$isImage ) {
446 $url = $this->core->files->get_path( $newFileId );
447 $data = $this->core->files->get_data( $newFileId );
448 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
449 $filename = basename( $url );
450
451 // Upload the file
452 $file = $openai->upload_file( $filename, $data, 'assistants' );
453
454 // Create a store
455 if ( empty( $storeId ) ) {
456 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
457 if ( !empty( $query->chatId ) ) {
458 $chatbotName .= "_" . $query->chatId;
459 }
460 $metadata = [];
461 if ( !empty( $chatbot['assistantId'] ) ) {
462 $metadata['assistantId'] = $chatbot['assistantId'];
463 }
464 if ( !empty( $query->chatId ) ) {
465 $metadata['chatId'] = $query->chatId;
466 }
467 $expiry = $this->core->get_option( 'image_expires' );
468 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
469 $query->setStoreId( $storeId );
470 }
471
472 // Add the file to the store
473 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
474
475 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
476 $openAiRefId = $file['id'];
477 $internalFileId = $this->core->files->get_id_from_refId( $newFileId );
478 $this->core->files->update_refId( $internalFileId, $openAiRefId );
479 $this->core->files->update_envId( $internalFileId, $query->envId );
480 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
481 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
482 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
483 $newFileId = $openAiRefId;
484 $scope = $params['fileSearch'];
485 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
486 $id = $this->core->files->get_id_from_refId( $newFileId );
487 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
488 }
489 }
490 else {
491 $url = $this->core->files->get_url( $newFileId );
492 $mimeType = $this->core->files->get_mime_type( $newFileId );
493 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
494 $purposeType = $isIMG ? 'vision' : 'files';
495 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
496 $fileId = $this->core->files->get_id_from_refId( $newFileId );
497 $this->core->files->update_envId( $fileId, $query->envId );
498 $this->core->files->update_purpose( $fileId, $purposeType );
499 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
500 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
501 }
502 }
503
504 // Takeover
505 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
506 if ( !empty( $takeoverAnswer ) ) {
507 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
508 return [
509 'reply' => $rawText,
510 'chatId' => $this->core->fix_chat_id( $query, $params ),
511 'images' => null,
512 'actions' => [],
513 'usage' => null
514 ];
515 }
516
517 // Moderation
518 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
519 $this->core->get_option( 'shortcode_chat_moderation' );
520 if ( $moderationEnabled ) {
521 global $mwai;
522 $isFlagged = $mwai->moderationCheck( $query->get_message() );
523 if ( $isFlagged ) {
524 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
525 }
526 }
527
528 // Setup streaming if enabled (before embeddings to capture those events)
529 $streamCallback = null;
530 if ( $stream ) {
531 $streamCallback = function( $reply ) use ( $query ) {
532 // Support both legacy string data and new Event objects
533 if ( is_string( $reply ) ) {
534 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
535 } else {
536 $this->core->stream_push( $reply, $query );
537 }
538 };
539 header( 'Cache-Control: no-cache' );
540 header( 'Content-Type: text/event-stream' );
541 // This is useful to disable buffering in nginx through headers.
542 header( 'X-Accel-Buffering: no' );
543 ob_implicit_flush( true );
544 ob_end_flush();
545 }
546
547 // Awareness & Embeddings
548 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
549 if ( !empty( $context ) ) {
550 $query->set_context( $context['content'] );
551 }
552
553 // Function Aware
554 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
555 }
556
557 // Process Query
558
559 $reply = $this->core->run_query( $query, $streamCallback, true );
560 $rawText = $reply->result;
561 $extra = [];
562 if ( $context ) {
563 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
564 }
565 // Add response ID to extra for discussion storage
566 if ( !empty( $reply->id ) ) {
567 $extra['responseId'] = $reply->id;
568 }
569 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
570
571 // Integrity Check: We need to store the checksum of the messages sent by the client.
572 $stored_messages = $client_messages;
573 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
574 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
575 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
576 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
577
578 // Actions
579 $actions = [];
580 if ( $reply->needClientActions ) {
581 foreach ( $reply->needClientActions as $action ) {
582 $actions[] = [
583 'type' => 'function',
584 'data' => [
585 'name' => $action['function']->name,
586 'args' => $action['arguments']
587 ]
588 ];
589 }
590 }
591
592 $restRes = [
593 'reply' => $rawText,
594 'chatId' => $this->core->fix_chat_id( $query, $params ),
595 'images' => $reply->get_type() === 'images' ? $reply->results : null,
596 'actions' => $actions,
597 'usage' => $reply->usage
598 ];
599
600 // Add response ID if available (for Responses API)
601 if ( !empty( $reply->id ) ) {
602 $restRes['responseId'] = $reply->id;
603 }
604
605 // Process Reply
606 if ( $stream ) {
607 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
608 $restRes['reply'], $restRes['images'], $restRes['actions'], $restRes['usage'],
609 $restRes['responseId'] ?? null );
610 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
611 die();
612 }
613 else {
614 return $restRes;
615 }
616
617 }
618 catch ( Exception $e ) {
619 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
620 if ( $stream ) {
621 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
622 die();
623 }
624 else {
625 throw $e;
626 }
627 }
628 }
629
630 public function inject_chat() {
631 $params = $this->core->get_chatbot( $this->siteWideChatId );
632 $clean_params = [];
633 if ( !empty( $params ) ) {
634 $clean_params['window'] = true;
635 $clean_params['id'] = $this->siteWideChatId;
636 echo $this->chat_shortcode( $clean_params );
637 }
638 return null;
639 }
640
641 public function build_front_params( $botId, $customId ) {
642 $frontSystem = [
643 'botId' => $customId ? null : $botId,
644 'customId' => $customId,
645 'userData' => $this->core->get_user_data(),
646 'sessionId' => $this->core->get_session_id(),
647 'restNonce' => $this->core->get_nonce(),
648 'contextId' => get_the_ID(),
649 'pluginUrl' => MWAI_URL,
650 'restUrl' => untrailingslashit( get_rest_url() ),
651 'stream' => $this->core->get_option( 'ai_streaming' ),
652 'debugMode' => $this->core->get_option('module_devtools') && $this->core->get_option( 'debug_mode' ),
653 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
654 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
655 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
656 'virtual_keyboard_fix' => $this->core->get_option( 'virtual_keyboard_fix' )
657 ];
658 return $frontSystem;
659 }
660
661 public function resolveBotInfo( &$atts )
662 {
663 $chatbot = null;
664 $botId = $atts['id'] ?? null;
665 $customId = $atts['custom_id'] ?? null;
666 $parentBotId = null;
667
668 if ( !$botId && !$customId ) {
669 $botId = "default";
670 }
671 if ( $botId ) {
672 $chatbot = $this->core->get_chatbot( $botId );
673 if (!$chatbot) {
674 $botId = $botId ?: 'N/A';
675 return [
676 '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'.",
677 ];
678 }
679 }
680 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
681 if ( !empty( $customId ) ) {
682 if ( $botId !== null ) {
683 $parentBotId = $botId;
684 $botId = null;
685 }
686 }
687 unset( $atts['id'] );
688 return [
689 'chatbot' => $chatbot,
690 'botId' => $botId,
691 'customId' => $customId,
692 'parentBotId' => $parentBotId
693 ];
694 }
695
696 public function chat_shortcode( $atts ) {
697 $atts = empty( $atts ) ? [] : $atts;
698
699 foreach ( $atts as $key => $value ) {
700 $atts[ $key ] = urldecode( $value );
701 }
702
703 // Let the user override the chatbot params
704 $atts = apply_filters( 'mwai_chatbot_params', $atts );
705
706 // Resolve the bot info
707 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
708 if ( isset( $resolvedBot['error'] ) ) {
709 return $resolvedBot['error'];
710 }
711 $chatbot = $resolvedBot['chatbot'];
712 $botId = $resolvedBot['botId'];
713 $customId = $resolvedBot['customId'];
714 $parentBotId = $resolvedBot['parentBotId'];
715
716 // Rename the keys of the atts into camelCase to match the internal params system.
717 $atts = array_map( function( $key, $value ) {
718 $key = str_replace( '_', ' ', $key );
719 $key = ucwords( $key );
720 $key = str_replace( ' ', '', $key );
721 $key = lcfirst( $key );
722 return [ $key => $value ];
723 }, array_keys( $atts ), $atts );
724 $atts = array_merge( ...$atts );
725
726 if ( !empty( $parentBotId ) ) {
727 $atts['parentBotId'] = $parentBotId;
728 }
729
730 $frontParams = [];
731 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
732 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
733 if ( isset( $atts[$param] ) ) {
734 if ( $param === 'localMemory' ) {
735 $frontParams[$param] = $atts[$param] === 'true';
736 }
737 else {
738 $frontParams[$param] = $atts[$param];
739 }
740 }
741 // If not, let's use the chatbot's default values
742 else if ( isset( $chatbot[$param] ) ) {
743 $frontParams[$param] = $chatbot[$param];
744 }
745
746 // Apply the placeholders
747 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
748 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
749 }
750 }
751
752 // Server Params
753 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
754 // we are using the default or a specific chatbot.
755 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
756 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
757 $hasFrontOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_FRONT_PARAMS ) ) > 0;
758 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasFrontOverrides );
759
760 $serverParams = [];
761 if ( $hasOverrides ) {
762 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
763 if ( isset( $atts[$param] ) ) {
764 $serverParams[$param] = $atts[$param];
765 }
766 else {
767 $serverParams[$param] = $chatbot[$param] ?? null;
768 }
769 }
770 }
771
772 // Front Params
773 $frontSystem = $this->build_front_params( $botId, $customId );
774
775 // Clean Params
776 $frontParams = $this->clean_params( $frontParams );
777 $frontSystem = $this->clean_params( $frontSystem );
778 $serverParams = $this->clean_params( $serverParams );
779
780 // Server-side: Keep the System Params
781 if ( $hasOverrides ) {
782 if ( empty( $customId ) ) {
783 $customId = md5( json_encode( $serverParams ) );
784 $frontSystem['customId'] = $customId;
785 }
786 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
787 }
788
789 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
790 $filterParams = [
791 'step' => 'init',
792 'botId' => $botId,
793 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
794 ];
795 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
796 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
797 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
798 $frontSystem['actions'] = $this->sanitize_actions( $actions );
799 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
800 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
801
802 // Client-side: Prepare JSON for Front Params and System Params
803 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
804 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
805 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
806 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
807 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
808
809 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
810
811 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
812 }
813
814 function chatbot_discussions( $atts ) {
815 $atts = empty($atts) ? [] : $atts;
816
817 // Resolve the bot info
818 $resolvedBot = $this->resolveBotInfo( $atts );
819 if ( isset( $resolvedBot['error'] ) ) {
820 return $resolvedBot['error'];
821 }
822 $chatbot = $resolvedBot['chatbot'];
823 $botId = $resolvedBot['botId'];
824 $customId = $resolvedBot['customId'];
825
826 // Rename the keys of the atts into camelCase to match the internal params system.
827 $atts = array_map( function( $key, $value ) {
828 $key = str_replace( '_', ' ', $key );
829 $key = ucwords( $key );
830 $key = str_replace( ' ', '', $key );
831 $key = lcfirst( $key );
832 return [ $key => $value ];
833 }, array_keys( $atts ), $atts );
834 $atts = array_merge( ...$atts );
835
836 // Front Params
837 $frontParams = [];
838 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
839 if ( isset( $atts[$param] ) ) {
840 $frontParams[$param] = $atts[$param];
841 }
842 else if ( isset( $chatbot[$param] ) ) {
843 $frontParams[$param] = $chatbot[$param];
844 }
845 }
846
847 // Server Params
848 $serverParams = [];
849 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
850 if ( isset( $atts[$param] ) ) {
851 $serverParams[$param] = $atts[$param];
852 }
853 }
854
855 // Front System
856 $frontSystem = $this->build_front_params( $botId, $customId );
857 // Get refresh interval from settings
858 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
859 if ( $refresh_interval === 'Never' ) {
860 $frontSystem['refreshInterval'] = 0;
861 } elseif ( $refresh_interval === 'Manual' ) {
862 $frontSystem['refreshInterval'] = -1;
863 } elseif ( is_numeric( $refresh_interval ) ) {
864 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
865 } else {
866 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
867 }
868 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
869
870 // Get paging setting
871 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
872 if ( $paging_option === 'None' ) {
873 $frontSystem['paging'] = 0; // No pagination
874 } else {
875 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
876 }
877
878 // Clean Params
879 $frontParams = $this->clean_params( $frontParams );
880 $frontSystem = $this->clean_params( $frontSystem );
881 $serverParams = $this->clean_params( $serverParams );
882
883 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
884 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
885 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
886 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
887
888 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
889 }
890
891 function clean_params( &$params ) {
892 foreach ( $params as $param => $value ) {
893 if ( $param === 'restNonce' ) {
894 continue;
895 }
896 if ( empty( $value ) || is_array( $value ) ) {
897 continue;
898 }
899 $lowerCaseValue = strtolower( $value );
900 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
901 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
902 }
903 else if ( is_numeric( $value ) ) {
904 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
905 }
906 }
907 return $params;
908 }
909
910 }
911