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