PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.3
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
855 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', '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 ) {
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 'callback' => ['label', 'onClick'],
198 ];
199 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
200 }
201
202 #region Messages Integrity Check
203
204 function messages_integrity_diff( $messages1, $messages2 ) {
205 // Collect messages with role not 'user' from messages1
206 $messagesList1 = array();
207 foreach( $messages1 as $msg ) {
208 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
209 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
210 if( $role && $role != 'user' ) {
211 $messageData = array( 'role' => $role, 'content' => $content );
212 $messagesList1[] = $messageData;
213 }
214 }
215
216 // Collect messages with role not 'user' from messages2
217 $messagesList2 = array();
218 foreach( $messages2 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 $messagesList2[] = $messageData;
224 }
225 }
226
227 // Count occurrences of each message in messagesList1
228 $counts1 = array();
229 foreach( $messagesList1 as $msg ) {
230 $key = serialize( $msg );
231 if( isset( $counts1[ $key ] ) ) {
232 $counts1[ $key ]++;
233 } else {
234 $counts1[ $key ] = 1;
235 }
236 }
237
238 // Count occurrences of each message in messagesList2
239 $counts2 = array();
240 foreach( $messagesList2 as $msg ) {
241 $key = serialize( $msg );
242 if( isset( $counts2[ $key ] ) ) {
243 $counts2[ $key ]++;
244 } else {
245 $counts2[ $key ] = 1;
246 }
247 }
248
249 // Compare counts to find unmatched messages
250 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
251
252 $diffs = array();
253 foreach( $all_keys as $key ) {
254 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
255 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
256 if( $count1 != $count2 ) {
257 $message = unserialize( $key );
258 $diffs[] = array(
259 'message' => $message,
260 'count_in_messages1' => $count1,
261 'count_in_messages2' => $count2
262 );
263 }
264 }
265
266 return $diffs;
267 }
268
269 private function calculate_messages_checksum( $messages ) {
270 $messages_to_hash = [];
271 foreach ( $messages as $msg ) {
272 $role = is_array( $msg ) ? ($msg['role'] ?? '') : (is_object( $msg ) ? ($msg->role ?? '') : '');
273 $content = is_array( $msg ) ? ($msg['content'] ?? '') : (is_object( $msg ) ? ($msg->content ?? '') : '');
274 if ( in_array( $role, ['assistant', 'system'] ) ) {
275 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
276 }
277 }
278 return md5( json_encode( $messages_to_hash ) );
279 }
280
281 #endregion
282
283 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false ) {
284 try {
285 $chatbot = null;
286 $customId = $params['customId'] ?? null;
287
288 // Custom Chatbot
289 if ( $customId ) {
290 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
291 }
292 // Registered Chatbot
293 if ( !$chatbot && $botId ) {
294 $chatbot = $this->core->get_chatbot( $botId );
295 }
296
297 if ( !$chatbot ) {
298 Meow_MWAI_Logging::warn( "The query was rejected - no chatbot was found." );
299 throw new Exception( 'Sorry, your query has been rejected.' );
300 }
301
302 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
303 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int)$textInputMaxLength ) {
304 Meow_MWAI_Logging::warn( "The query was rejected - message was too long." );
305 throw new Exception( 'Sorry, your query has been rejected.' );
306 }
307
308 // We need to check the integrity of the messages sent by the client.
309 // This is important to ensure that the messages are not tampered with.
310
311 // Messages Integrity Check with Checksums
312 $chatId = $params['chatId'] ?? 'default';
313 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
314 $stored_checksum = get_transient( $checksum_key );
315 $client_messages = $params['messages'] ?? [];
316 $client_checksum = $this->calculate_messages_checksum( $client_messages );
317 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
318 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." );
319 }
320
321 // Messages Integrity Check with Discussions
322 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
323 $discussion = $this->core->discussions->get_discussion( $botId ? $botId: $customId, $params['chatId'] );
324 if ( $discussion ) {
325 $messages = $discussion['messages'];
326 $diffs = $this->messages_integrity_diff( $messages, $params['messages'] );
327 if ( count( $diffs ) > 0 ) {
328 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
329 }
330 }
331 else {
332 // No discussion yet? We still need to check the startSentence.
333 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
334 $messages = [];
335 if ( !empty( $startSentence ) ) {
336 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
337 }
338 $diffs = $this->messages_integrity_diff( $messages, $params['messages'] );
339 if ( count( $diffs ) > 0 ) {
340 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 ) );
341 }
342 }
343 }
344
345 // Create QueryText
346 $context = null;
347 $mode = $chatbot['mode'] ?? 'chat';
348
349 if ( $mode === 'images' ) {
350 $query = new Meow_MWAI_Query_Image( $newMessage );
351
352 // Handle Params
353 $newParams = [];
354 foreach ( $chatbot as $key => $value ) {
355 $newParams[$key] = $value;
356 }
357 foreach ( $params as $key => $value ) {
358 $newParams[$key] = $value;
359 }
360 $params = apply_filters( 'mwai_chatbot_params', $newParams );
361 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
362 $query->inject_params( $params );
363 }
364 else {
365 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
366 new Meow_MWAI_Query_Text( $newMessage, 1024 );
367 $streamCallback = null;
368
369 // Handle Params
370 $newParams = [];
371 foreach ( $chatbot as $key => $value ) {
372 $newParams[$key] = $value;
373 }
374 foreach ( $params as $key => $value ) {
375 $newParams[$key] = $value;
376 }
377 $params = apply_filters( 'mwai_chatbot_params', $newParams );
378 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
379 $query->inject_params( $params );
380
381 $storeId = null;
382 if ( $mode === 'assistant' ) {
383 $chatId = $params['chatId'] ?? null;
384 if ( !empty( $chatId ) ) {
385 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
386 if ( isset( $discussion['storeId'] ) ) {
387 $storeId = $discussion['storeId'];
388 $query->setStoreId( $storeId );
389 }
390 }
391 }
392
393 // Support for Uploaded Image
394 if ( !empty( $newFileId ) ) {
395
396 // Get extension and mime type
397 $isImage = $this->core->files->is_image( $newFileId );
398
399 if ( $mode === 'assistant' && !$isImage ) {
400 $url = $this->core->files->get_path( $newFileId );
401 $data = $this->core->files->get_data( $newFileId );
402 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
403 $filename = basename( $url );
404
405 // Upload the file
406 $file = $openai->upload_file( $filename, $data, 'assistants' );
407
408 // Create a store
409 if ( empty( $storeId ) ) {
410 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
411 if ( !empty( $query->chatId ) ) {
412 $chatbotName .= "_" . $query->chatId;
413 }
414 $metadata = [];
415 if ( !empty( $chatbot['assistantId'] ) ) {
416 $metadata['assistantId'] = $chatbot['assistantId'];
417 }
418 if ( !empty( $query->chatId ) ) {
419 $metadata['chatId'] = $query->chatId;
420 }
421 $expiry = $this->core->get_option( 'image_expires' );
422 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
423 $query->setStoreId( $storeId );
424 }
425
426 // Add the file to the store
427 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
428
429 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
430 $openAiRefId = $file['id'];
431 $internalFileId = $this->core->files->get_id_from_refId( $newFileId );
432 $this->core->files->update_refId( $internalFileId, $openAiRefId );
433 $this->core->files->update_envId( $internalFileId, $query->envId );
434 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
435 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
436 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
437 $newFileId = $openAiRefId;
438 $scope = $params['fileSearch'];
439 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
440 $id = $this->core->files->get_id_from_refId( $newFileId );
441 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
442 }
443 }
444 else {
445 $url = $this->core->files->get_url( $newFileId );
446 $mimeType = $this->core->files->get_mime_type( $newFileId );
447 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
448 $purposeType = $isIMG ? 'vision' : 'files';
449 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
450 $fileId = $this->core->files->get_id_from_refId( $newFileId );
451 $this->core->files->update_envId( $fileId, $query->envId );
452 $this->core->files->update_purpose( $fileId, $purposeType );
453 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
454 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
455 }
456 }
457
458 // Takeover
459 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
460 if ( !empty( $takeoverAnswer ) ) {
461 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
462 return [
463 'reply' => $rawText,
464 'chatId' => $this->core->fix_chat_id( $query, $params ),
465 'images' => null,
466 'actions' => [],
467 'usage' => null
468 ];
469 }
470
471 // Moderation
472 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
473 $this->core->get_option( 'shortcode_chat_moderation' );
474 if ( $moderationEnabled ) {
475 global $mwai;
476 $isFlagged = $mwai->moderationCheck( $query->get_message() );
477 if ( $isFlagged ) {
478 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
479 }
480 }
481
482 // Setup streaming if enabled (before embeddings to capture those events)
483 $streamCallback = null;
484 if ( $stream ) {
485 $streamCallback = function( $reply ) use ( $query ) {
486 // Support both legacy string data and new Event objects
487 if ( is_string( $reply ) ) {
488 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
489 } else {
490 $this->core->stream_push( $reply, $query );
491 }
492 };
493 header( 'Cache-Control: no-cache' );
494 header( 'Content-Type: text/event-stream' );
495 // This is useful to disable buffering in nginx through headers.
496 header( 'X-Accel-Buffering: no' );
497 ob_implicit_flush( true );
498 ob_end_flush();
499 }
500
501 // Awareness & Embeddings
502 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
503 if ( !empty( $context ) ) {
504 $query->set_context( $context['content'] );
505 }
506
507 // Function Aware
508 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
509 }
510
511 // Process Query
512
513 $reply = $this->core->run_query( $query, $streamCallback, true );
514 $rawText = $reply->result;
515 $extra = [];
516 if ( $context ) {
517 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
518 }
519 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
520
521 // Integrity Check: We need to store the checksum of the messages sent by the client.
522 $stored_messages = $client_messages;
523 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
524 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
525 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
526 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
527
528 // Actions
529 $actions = [];
530 if ( $reply->needClientActions ) {
531 foreach ( $reply->needClientActions as $action ) {
532 $actions[] = [
533 'type' => 'function',
534 'data' => [
535 'name' => $action['function']->name,
536 'args' => $action['arguments']
537 ]
538 ];
539 }
540 }
541
542 $restRes = [
543 'reply' => $rawText,
544 'chatId' => $this->core->fix_chat_id( $query, $params ),
545 'images' => $reply->get_type() === 'images' ? $reply->results : null,
546 'actions' => $actions,
547 'usage' => $reply->usage
548 ];
549
550 // Process Reply
551 if ( $stream ) {
552 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
553 $restRes['reply'], $restRes['images'], $restRes['actions'], $restRes['usage'] );
554 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
555 die();
556 }
557 else {
558 return $restRes;
559 }
560
561 }
562 catch ( Exception $e ) {
563 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
564 if ( $stream ) {
565 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
566 die();
567 }
568 else {
569 throw $e;
570 }
571 }
572 }
573
574 public function inject_chat() {
575 $params = $this->core->get_chatbot( $this->siteWideChatId );
576 $clean_params = [];
577 if ( !empty( $params ) ) {
578 $clean_params['window'] = true;
579 $clean_params['id'] = $this->siteWideChatId;
580 echo $this->chat_shortcode( $clean_params );
581 }
582 return null;
583 }
584
585 public function build_front_params( $botId, $customId ) {
586 $frontSystem = [
587 'botId' => $customId ? null : $botId,
588 'customId' => $customId,
589 'userData' => $this->core->get_user_data(),
590 'sessionId' => $this->core->get_session_id(),
591 'restNonce' => $this->core->get_nonce(),
592 'contextId' => get_the_ID(),
593 'pluginUrl' => MWAI_URL,
594 'restUrl' => untrailingslashit( get_rest_url() ),
595 'stream' => $this->core->get_option( 'ai_streaming' ),
596 'debugMode' => $this->core->get_option('module_devtools') && $this->core->get_option( 'debug_mode' ),
597 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
598 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
599 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
600 'virtual_keyboard_fix' => $this->core->get_option( 'virtual_keyboard_fix' )
601 ];
602 return $frontSystem;
603 }
604
605 public function resolveBotInfo( &$atts )
606 {
607 $chatbot = null;
608 $botId = $atts['id'] ?? null;
609 $customId = $atts['custom_id'] ?? null;
610 $parentBotId = null;
611
612 if ( !$botId && !$customId ) {
613 $botId = "default";
614 }
615 if ( $botId ) {
616 $chatbot = $this->core->get_chatbot( $botId );
617 if (!$chatbot) {
618 $botId = $botId ?: 'N/A';
619 return [
620 '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'.",
621 ];
622 }
623 }
624 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
625 if ( !empty( $customId ) ) {
626 if ( $botId !== null ) {
627 $parentBotId = $botId;
628 $botId = null;
629 }
630 }
631 unset( $atts['id'] );
632 return [
633 'chatbot' => $chatbot,
634 'botId' => $botId,
635 'customId' => $customId,
636 'parentBotId' => $parentBotId
637 ];
638 }
639
640 public function chat_shortcode( $atts ) {
641 $atts = empty( $atts ) ? [] : $atts;
642
643 foreach ( $atts as $key => $value ) {
644 $atts[ $key ] = urldecode( $value );
645 }
646
647 // Let the user override the chatbot params
648 $atts = apply_filters( 'mwai_chatbot_params', $atts );
649
650 // Resolve the bot info
651 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
652 if ( isset( $resolvedBot['error'] ) ) {
653 return $resolvedBot['error'];
654 }
655 $chatbot = $resolvedBot['chatbot'];
656 $botId = $resolvedBot['botId'];
657 $customId = $resolvedBot['customId'];
658 $parentBotId = $resolvedBot['parentBotId'];
659
660 // Rename the keys of the atts into camelCase to match the internal params system.
661 $atts = array_map( function( $key, $value ) {
662 $key = str_replace( '_', ' ', $key );
663 $key = ucwords( $key );
664 $key = str_replace( ' ', '', $key );
665 $key = lcfirst( $key );
666 return [ $key => $value ];
667 }, array_keys( $atts ), $atts );
668 $atts = array_merge( ...$atts );
669
670 if ( !empty( $parentBotId ) ) {
671 $atts['parentBotId'] = $parentBotId;
672 }
673
674 $frontParams = [];
675 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
676 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
677 if ( isset( $atts[$param] ) ) {
678 if ( $param === 'localMemory' ) {
679 $frontParams[$param] = $atts[$param] === 'true';
680 }
681 else {
682 $frontParams[$param] = $atts[$param];
683 }
684 }
685 // If not, let's use the chatbot's default values
686 else if ( isset( $chatbot[$param] ) ) {
687 $frontParams[$param] = $chatbot[$param];
688 }
689
690 // Apply the placeholders
691 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
692 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
693 }
694 }
695
696 // Server Params
697 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
698 // we are using the default or a specific chatbot.
699 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
700 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
701 $hasFrontOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_FRONT_PARAMS ) ) > 0;
702 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasFrontOverrides );
703
704 $serverParams = [];
705 if ( $hasOverrides ) {
706 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
707 if ( isset( $atts[$param] ) ) {
708 $serverParams[$param] = $atts[$param];
709 }
710 else {
711 $serverParams[$param] = $chatbot[$param] ?? null;
712 }
713 }
714 }
715
716 // Front Params
717 $frontSystem = $this->build_front_params( $botId, $customId );
718
719 // Clean Params
720 $frontParams = $this->clean_params( $frontParams );
721 $frontSystem = $this->clean_params( $frontSystem );
722 $serverParams = $this->clean_params( $serverParams );
723
724 // Server-side: Keep the System Params
725 if ( $hasOverrides ) {
726 if ( empty( $customId ) ) {
727 $customId = md5( json_encode( $serverParams ) );
728 $frontSystem['customId'] = $customId;
729 }
730 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
731 }
732
733 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
734 $filterParams = [
735 'step' => 'init',
736 'botId' => $botId,
737 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
738 ];
739 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
740 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
741 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
742 $frontSystem['actions'] = $this->sanitize_actions( $actions );
743 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
744 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
745
746 // Client-side: Prepare JSON for Front Params and System Params
747 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
748 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
749 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
750 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
751 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
752
753 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
754
755 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
756 }
757
758 function chatbot_discussions( $atts ) {
759 $atts = empty($atts) ? [] : $atts;
760
761 // Resolve the bot info
762 $resolvedBot = $this->resolveBotInfo( $atts );
763 if ( isset( $resolvedBot['error'] ) ) {
764 return $resolvedBot['error'];
765 }
766 $chatbot = $resolvedBot['chatbot'];
767 $botId = $resolvedBot['botId'];
768 $customId = $resolvedBot['customId'];
769
770 // Rename the keys of the atts into camelCase to match the internal params system.
771 $atts = array_map( function( $key, $value ) {
772 $key = str_replace( '_', ' ', $key );
773 $key = ucwords( $key );
774 $key = str_replace( ' ', '', $key );
775 $key = lcfirst( $key );
776 return [ $key => $value ];
777 }, array_keys( $atts ), $atts );
778 $atts = array_merge( ...$atts );
779
780 // Front Params
781 $frontParams = [];
782 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
783 if ( isset( $atts[$param] ) ) {
784 $frontParams[$param] = $atts[$param];
785 }
786 else if ( isset( $chatbot[$param] ) ) {
787 $frontParams[$param] = $chatbot[$param];
788 }
789 }
790
791 // Server Params
792 $serverParams = [];
793 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
794 if ( isset( $atts[$param] ) ) {
795 $serverParams[$param] = $atts[$param];
796 }
797 }
798
799 // Front System
800 $frontSystem = $this->build_front_params( $botId, $customId );
801 // Get refresh interval from settings
802 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
803 if ( $refresh_interval === 'Never' ) {
804 $frontSystem['refreshInterval'] = 0;
805 } elseif ( $refresh_interval === 'Manual' ) {
806 $frontSystem['refreshInterval'] = -1;
807 } elseif ( is_numeric( $refresh_interval ) ) {
808 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
809 } else {
810 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
811 }
812 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
813
814 // Get paging setting
815 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
816 if ( $paging_option === 'None' ) {
817 $frontSystem['paging'] = 0; // No pagination
818 } else {
819 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
820 }
821
822 // Clean Params
823 $frontParams = $this->clean_params( $frontParams );
824 $frontSystem = $this->clean_params( $frontSystem );
825 $serverParams = $this->clean_params( $serverParams );
826
827 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
828 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
829 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
830 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
831
832 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
833 }
834
835 function clean_params( &$params ) {
836 foreach ( $params as $param => $value ) {
837 if ( $param === 'restNonce' ) {
838 continue;
839 }
840 if ( empty( $value ) || is_array( $value ) ) {
841 continue;
842 }
843 $lowerCaseValue = strtolower( $value );
844 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
845 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
846 }
847 else if ( is_numeric( $value ) ) {
848 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
849 }
850 }
851 return $params;
852 }
853
854 }
855