PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.0
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
838 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', 'mode',
9 'textInputPlaceholder', 'textInputMaxLength', 'textCompliance', 'startSentence', 'localMemory',
10 'themeId', 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', 'iconBubble',
11 'fullscreen', 'copyButton', 'headerSubtitle'
12 ] );
13
14 define( 'MWAI_CHATBOT_SERVER_PARAMS', [ 'id', 'envId', 'scope', 'mode', 'contentAware', 'context', 'startSentence',
15 'embeddingsEnvId', 'embeddingsIndex', 'embeddingsNamespace', 'assistantId', 'instructions', 'resolution', 'voice',
16 'model', 'temperature', 'maxTokens', 'contextMaxLength', 'maxResults', 'apiKey', 'functions', 'parentBotId'
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 '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 // Awareness & Embeddings
483 $context = $this->core->retrieve_context( $params, $query );
484 if ( !empty( $context ) ) {
485 $query->set_context( $context['content'] );
486 }
487
488 // Function Aware
489 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
490 }
491
492 // Process Query
493 if ( $stream ) {
494 $streamCallback = function( $reply ) use ( $query ) {
495 $raw = $reply;
496 $this->core->stream_push( [ 'type' => 'live', 'data' => $raw ], $query );
497 // if ( ob_get_level() > 0 ) {
498 // ob_flush();
499 // }
500 // flush();
501 };
502 header( 'Cache-Control: no-cache' );
503 header( 'Content-Type: text/event-stream' );
504 // This is useful to disable buffering in nginx through headers.
505 header( 'X-Accel-Buffering: no' );
506 ob_implicit_flush( true );
507 ob_end_flush();
508 }
509
510 $reply = $this->core->run_query( $query, $streamCallback, true );
511 $rawText = $reply->result;
512 $extra = [];
513 if ( $context ) {
514 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
515 }
516 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
517
518 // Integrity Check: We need to store the checksum of the messages sent by the client.
519 $stored_messages = $client_messages;
520 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
521 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
522 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
523 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
524
525 // Actions
526 $actions = [];
527 if ( $reply->needClientActions ) {
528 foreach ( $reply->needClientActions as $action ) {
529 $actions[] = [
530 'type' => 'function',
531 'data' => [
532 'name' => $action['function']->name,
533 'args' => $action['arguments']
534 ]
535 ];
536 }
537 }
538
539 $restRes = [
540 'reply' => $rawText,
541 'chatId' => $this->core->fix_chat_id( $query, $params ),
542 'images' => $reply->get_type() === 'images' ? $reply->results : null,
543 'actions' => $actions,
544 'usage' => $reply->usage
545 ];
546
547 // Process Reply
548 if ( $stream ) {
549 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
550 $restRes['reply'], $restRes['images'], $restRes['actions'], $restRes['usage'] );
551 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
552 die();
553 }
554 else {
555 return $restRes;
556 }
557
558 }
559 catch ( Exception $e ) {
560 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
561 if ( $stream ) {
562 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
563 die();
564 }
565 else {
566 throw $e;
567 }
568 }
569 }
570
571 public function inject_chat() {
572 $params = $this->core->get_chatbot( $this->siteWideChatId );
573 $clean_params = [];
574 if ( !empty( $params ) ) {
575 $clean_params['window'] = true;
576 $clean_params['id'] = $this->siteWideChatId;
577 echo $this->chat_shortcode( $clean_params );
578 }
579 return null;
580 }
581
582 public function build_front_params( $botId, $customId ) {
583 $frontSystem = [
584 'botId' => $customId ? null : $botId,
585 'customId' => $customId,
586 'userData' => $this->core->get_user_data(),
587 'sessionId' => $this->core->get_session_id(),
588 'restNonce' => $this->core->get_nonce(),
589 'contextId' => get_the_ID(),
590 'pluginUrl' => MWAI_URL,
591 'restUrl' => untrailingslashit( get_rest_url() ),
592 'stream' => $this->core->get_option( 'ai_streaming' ),
593 'debugMode' => $this->core->get_option('module_devtools') && $this->core->get_option( 'debug_mode' ),
594 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
595 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
596 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
597 'virtual_keyboard_fix' => $this->core->get_option( 'virtual_keyboard_fix' )
598 ];
599 return $frontSystem;
600 }
601
602 public function resolveBotInfo( &$atts )
603 {
604 $chatbot = null;
605 $botId = $atts['id'] ?? null;
606 $customId = $atts['custom_id'] ?? null;
607 $parentBotId = null;
608
609 if ( !$botId && !$customId ) {
610 $botId = "default";
611 }
612 if ( $botId ) {
613 $chatbot = $this->core->get_chatbot( $botId );
614 if (!$chatbot) {
615 $botId = $botId ?: 'N/A';
616 return [
617 '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'.",
618 ];
619 }
620 }
621 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
622 if ( !empty( $customId ) ) {
623 if ( $botId !== null ) {
624 $parentBotId = $botId;
625 $botId = null;
626 }
627 }
628 unset( $atts['id'] );
629 return [
630 'chatbot' => $chatbot,
631 'botId' => $botId,
632 'customId' => $customId,
633 'parentBotId' => $parentBotId
634 ];
635 }
636
637 // TODO: After January 2025, remove this.
638 public function old_chat_shortcode( $atts ) {
639 Meow_MWAI_Logging::deprecated( "The shortcode 'mwai_chatbot_v2' is deprecated. Please use 'mwai_chatbot' instead." );
640 return $this->chat_shortcode( $atts );
641 }
642
643 public function chat_shortcode( $atts ) {
644 $atts = empty( $atts ) ? [] : $atts;
645
646 foreach ( $atts as $key => $value ) {
647 $atts[ $key ] = urldecode( $value );
648 }
649
650 // Let the user override the chatbot params
651 $atts = apply_filters( 'mwai_chatbot_params', $atts );
652
653 // Resolve the bot info
654 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
655 if ( isset( $resolvedBot['error'] ) ) {
656 return $resolvedBot['error'];
657 }
658 $chatbot = $resolvedBot['chatbot'];
659 $botId = $resolvedBot['botId'];
660 $customId = $resolvedBot['customId'];
661 $parentBotId = $resolvedBot['parentBotId'];
662
663 // Rename the keys of the atts into camelCase to match the internal params system.
664 $atts = array_map( function( $key, $value ) {
665 $key = str_replace( '_', ' ', $key );
666 $key = ucwords( $key );
667 $key = str_replace( ' ', '', $key );
668 $key = lcfirst( $key );
669 return [ $key => $value ];
670 }, array_keys( $atts ), $atts );
671 $atts = array_merge( ...$atts );
672
673 if ( !empty( $parentBotId ) ) {
674 $atts['parentBotId'] = $parentBotId;
675 }
676
677 $frontParams = [];
678 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
679 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
680 if ( isset( $atts[$param] ) ) {
681 if ( $param === 'localMemory' ) {
682 $frontParams[$param] = $atts[$param] === 'true';
683 }
684 else {
685 $frontParams[$param] = $atts[$param];
686 }
687 }
688 // If not, let's use the chatbot's default values
689 else if ( isset( $chatbot[$param] ) ) {
690 $frontParams[$param] = $chatbot[$param];
691 }
692
693 // Apply the placeholders
694 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
695 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
696 }
697 }
698
699 // Server Params
700 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
701 // we are using the default or a specific chatbot.
702 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
703 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
704 $hasFrontOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_FRONT_PARAMS ) ) > 0;
705 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasFrontOverrides );
706
707 $serverParams = [];
708 if ( $hasOverrides ) {
709 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
710 if ( isset( $atts[$param] ) ) {
711 $serverParams[$param] = $atts[$param];
712 }
713 else {
714 $serverParams[$param] = $chatbot[$param] ?? null;
715 }
716 }
717 }
718
719 // Front Params
720 $frontSystem = $this->build_front_params( $botId, $customId );
721
722 // Clean Params
723 $frontParams = $this->clean_params( $frontParams );
724 $frontSystem = $this->clean_params( $frontSystem );
725 $serverParams = $this->clean_params( $serverParams );
726
727 // Server-side: Keep the System Params
728 if ( $hasOverrides ) {
729 if ( empty( $customId ) ) {
730 $customId = md5( json_encode( $serverParams ) );
731 $frontSystem['customId'] = $customId;
732 }
733 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
734 }
735
736 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
737 $filterParams = [
738 'step' => 'init',
739 'botId' => $botId,
740 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
741 ];
742 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
743 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
744 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
745 $frontSystem['actions'] = $this->sanitize_actions( $actions );
746 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
747 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
748
749 // Client-side: Prepare JSON for Front Params and System Params
750 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
751 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
752 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
753 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
754 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
755
756 $this->enqueue_scripts();
757
758 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
759 }
760
761 function chatbot_discussions( $atts ) {
762 $atts = empty($atts) ? [] : $atts;
763
764 // Resolve the bot info
765 $resolvedBot = $this->resolveBotInfo( $atts );
766 if ( isset( $resolvedBot['error'] ) ) {
767 return $resolvedBot['error'];
768 }
769 $chatbot = $resolvedBot['chatbot'];
770 $botId = $resolvedBot['botId'];
771 $customId = $resolvedBot['customId'];
772
773 // Rename the keys of the atts into camelCase to match the internal params system.
774 $atts = array_map( function( $key, $value ) {
775 $key = str_replace( '_', ' ', $key );
776 $key = ucwords( $key );
777 $key = str_replace( ' ', '', $key );
778 $key = lcfirst( $key );
779 return [ $key => $value ];
780 }, array_keys( $atts ), $atts );
781 $atts = array_merge( ...$atts );
782
783 // Front Params
784 $frontParams = [];
785 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
786 if ( isset( $atts[$param] ) ) {
787 $frontParams[$param] = $atts[$param];
788 }
789 else if ( isset( $chatbot[$param] ) ) {
790 $frontParams[$param] = $chatbot[$param];
791 }
792 }
793
794 // Server Params
795 $serverParams = [];
796 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
797 if ( isset( $atts[$param] ) ) {
798 $serverParams[$param] = $atts[$param];
799 }
800 }
801
802 // Front System
803 $frontSystem = $this->build_front_params( $botId, $customId );
804
805 // Clean Params
806 $frontParams = $this->clean_params( $frontParams );
807 $frontSystem = $this->clean_params( $frontSystem );
808 $serverParams = $this->clean_params( $serverParams );
809
810 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
811 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
812 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
813 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
814
815 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
816 }
817
818 function clean_params( &$params ) {
819 foreach ( $params as $param => $value ) {
820 if ( $param === 'restNonce' ) {
821 continue;
822 }
823 if ( empty( $value ) || is_array( $value ) ) {
824 continue;
825 }
826 $lowerCaseValue = strtolower( $value );
827 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
828 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
829 }
830 else if ( is_numeric( $value ) ) {
831 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
832 }
833 }
834 return $params;
835 }
836
837 }
838