PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.4.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.4.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 2 years ago security.php 2 years ago tasks.php 2 years ago wand.php 2 years ago
chatbot.php
680 lines
1 <?php
2
3 // Params for the chatbot (front and server)
4 define( 'MWAI_CHATBOT_FRONT_PARAMS', [ 'id', 'customId',
5 'aiName', 'userName', 'guestName',
6 'aiAvatar', 'userAvatar', 'guestAvatar',
7 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl',
8 'textSend', 'textClear', 'imageUpload', 'fileSearch',
9 'textInputPlaceholder', 'textInputMaxLength', 'textCompliance', 'startSentence', 'localMemory',
10 'themeId', 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', 'iconBubble',
11 'fullscreen', 'copyButton'
12 ] );
13 define( 'MWAI_CHATBOT_SERVER_PARAMS', [ 'id', 'envId', 'scope', 'mode', 'contentAware', 'context',
14 'embeddingsEnvId', 'embeddingsIndex', 'embeddingsNamespace', 'assistantId', 'instructions',
15 'model', 'temperature', 'maxTokens', 'contextMaxLength', 'maxResults', 'apiKey', 'functions'
16 ] );
17
18 // Params for the discussions (front and server)
19 define( 'MWAI_DISCUSSIONS_FRONT_PARAMS', [ 'themeId', 'textNewChat' ] );
20 define( 'MWAI_DISCUSSIONS_SERVER_PARAMS', [ 'customId' ] );
21
22 class Meow_MWAI_Modules_Chatbot {
23 private $core = null;
24 private $namespace = 'mwai-ui/v1';
25 private $siteWideChatId = null;
26
27 public function __construct() {
28 global $mwai_core;
29 $this->core = $mwai_core;
30 $this->siteWideChatId = $this->core->get_option( 'botId' );
31
32 add_shortcode( 'mwai_chatbot', array( $this, 'chat_shortcode' ) );
33 add_shortcode( 'mwai_chatbot_v2', array( $this, 'old_chat_shortcode' ) );
34 add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
35 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
36 add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
37 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
38 add_shortcode( 'mwai_discussions', [ $this, 'chatbot_discussions' ] );
39 }
40 }
41
42 public function register_scripts() {
43
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 // Load CSS for the Themes
51 $themes = $this->core->get_themes();
52 foreach ( $themes as $theme ) {
53 if ( $theme['type'] === 'internal' ) {
54 $themeId = $theme['themeId'];
55 $filename = $themeId . '.css';
56 $physical_file = trailingslashit( MWAI_PATH ) . 'themes/' . $filename;
57 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
58 wp_register_style( 'mwai_chatbot_theme_' . $themeId, trailingslashit( MWAI_URL )
59 . 'themes/' . $filename, [], $cache_buster );
60 }
61 }
62
63 // Actual loading of the scripts
64 $hasSiteWideChat = $this->siteWideChatId && $this->siteWideChatId !== 'none';
65 if ( is_admin() || $hasSiteWideChat ) {
66 $this->enqueue_scripts();
67 if ( $hasSiteWideChat ) {
68 // Chatbot Injection
69 add_action( 'wp_footer', array( $this, 'inject_chat' ) );
70 }
71 }
72 }
73
74 public function enqueue_scripts() {
75 // TODO: We should optimize and only load the themes that are used.
76 $themes = $this->core->get_themes();
77 foreach ( $themes as $theme ) {
78 if ( $theme['type'] === 'internal' ) {
79 $themeId = $theme['themeId'];
80 wp_enqueue_style( "mwai_chatbot_theme_$themeId" );
81 }
82 }
83 wp_enqueue_script( "mwai_chatbot" );
84 if ( $this->core->get_option( 'syntax_highlight' ) ) {
85 wp_enqueue_script( "mwai_highlight" );
86 }
87 }
88
89 public function rest_api_init() {
90 register_rest_route( $this->namespace, '/chats/submit', array(
91 'methods' => 'POST',
92 'callback' => [ $this, 'rest_chat' ],
93 'permission_callback' => array( $this->core, 'check_rest_nonce' )
94 ) );
95 }
96
97 public function basics_security_check( $botId, $customId, $newMessage, $newFileId ) {
98 if ( !$botId && !$customId ) {
99 $this->core->log( "⚠️ The query was rejected - no botId nor id was specified.");
100 return false;
101 }
102
103 if ( $newFileId ) {
104 return true;
105 }
106
107 $length = strlen( empty( $newMessage ) ? "" : $newMessage );
108 if ( $length < 1 || $length > ( 4096 * 16 ) ) {
109 $this->core->log( "⚠️ The query was rejected - message was too short or too long.");
110 return false;
111 }
112 return true;
113 }
114
115 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage ) {
116 $filterParams = [
117 'botId' => $botId,
118 'reply' => $reply,
119 'images' => $images,
120 'newMessage' => $newMessage,
121 'newFileId' => $newFileId,
122 'params' => $params,
123 'usage' => $usage,
124 ];
125 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
126 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
127 $actions = $this->sanitize_actions( $actions );
128 $blocks = $this->sanitize_blocks( $blocks );
129 return [
130 'success' => true,
131 'reply' => $reply,
132 'images' => $images,
133 'actions' => $actions,
134 'blocks' => $blocks,
135 'usage' => $usage
136 ];
137 }
138
139 public function rest_chat( $request ) {
140 $params = $request->get_json_params();
141 $botId = $params['botId'] ?? null;
142 $customId = $params['customId'] ?? null;
143 $stream = $params['stream'] ?? false;
144 $newMessage = trim( $params['newMessage'] ?? '' );
145 $newFileId = $params['newFileId'] ?? null;
146
147 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId )) {
148 return new WP_REST_Response( [
149 'success' => false,
150 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
151 ], 403 );
152 }
153
154 try {
155 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream );
156 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
157 $data['reply'], $data['images'], $data['actions'], $data['usage'] );
158 return new WP_REST_Response( $final_res, 200 );
159 }
160 catch ( Exception $e ) {
161 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
162 return new WP_REST_Response( [
163 'success' => false,
164 'message' => $message
165 ], 500 );
166 }
167 }
168
169 private function sanitize_items( $items, $supported_types, $type_name ) {
170 $sanitized_items = [];
171 foreach ( $items as $item ) {
172 if ( isset( $supported_types[$item['type']] ) ) {
173 $is_valid = true;
174 foreach ( $supported_types[$item['type']] as $param ) {
175 if ( empty( $item[$param] ) ) {
176 $is_valid = false;
177 $this->core->log( "⚠️ Missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
178 break;
179 }
180 }
181 if ( $is_valid ) {
182 $sanitized_items[] = $item;
183 }
184 }
185 else {
186 $this->core->log( "⚠️ Unsupported {$type_name} type: {$item['type']}." );
187 }
188 }
189
190 return $sanitized_items;
191 }
192
193 public function sanitize_actions( $actions ) {
194 $supported_action_types = [
195 'function' => ['name', 'args'],
196 'javascript' => ['snippet'],
197 'reply-shortcut' => ['label', 'content'],
198 ];
199 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
200 }
201
202 public function sanitize_blocks( $blocks ) {
203 $supported_block_types = [
204 'html' => ['content'],
205 ];
206 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
207 }
208
209 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false ) {
210 try {
211 $chatbot = null;
212 $customId = $params['customId'] ?? null;
213
214 // Custom Chatbot
215 if ( $customId ) {
216 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
217 }
218 // Registered Chatbot
219 if ( !$chatbot && $botId ) {
220 $chatbot = $this->core->get_chatbot( $botId );
221 }
222
223 if ( !$chatbot ) {
224 $this->core->log( "⚠️ No chatbot was found for this query.");
225 throw new Exception( 'Sorry, your query has been rejected.' );
226 }
227
228 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
229 if ( $textInputMaxLength && strlen( $newMessage ) > (int)$textInputMaxLength ) {
230 throw new Exception( 'Sorry, your query has been rejected.' );
231 }
232
233 // Create QueryText
234 $context = null;
235 $mode = $chatbot['mode'] ?? 'chat';
236
237 if ( $mode === 'images' ) {
238 $query = new Meow_MWAI_Query_Image( $newMessage );
239
240 // Handle Params
241 $newParams = [];
242 foreach ( $chatbot as $key => $value ) {
243 $newParams[$key] = $value;
244 }
245 foreach ( $params as $key => $value ) {
246 $newParams[$key] = $value;
247 }
248 $params = apply_filters( 'mwai_chatbot_params', $newParams );
249 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
250 $query->inject_params( $params );
251 }
252 else {
253 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
254 new Meow_MWAI_Query_Text( $newMessage, 1024 );
255 $streamCallback = null;
256
257 // Handle Params
258 $newParams = [];
259 foreach ( $chatbot as $key => $value ) {
260 $newParams[$key] = $value;
261 }
262 foreach ( $params as $key => $value ) {
263 $newParams[$key] = $value;
264 }
265 $params = apply_filters( 'mwai_chatbot_params', $newParams );
266 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
267 $query->inject_params( $params );
268
269 $storeId = null;
270 if ( $mode === 'assistant' ) {
271 $chatId = $params['chatId'] ?? null;
272 if ( !empty( $chatId ) ) {
273 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
274 if ( isset( $discussion['storeId'] ) ) {
275 $storeId = $discussion['storeId'];
276 $query->setStoreId( $storeId );
277 }
278 }
279 }
280
281 // Support for Uploaded Image
282 if ( !empty( $newFileId ) ) {
283
284 // Get extension and mime type
285 $isImage = $this->core->files->is_image( $newFileId );
286
287 if ( $mode === 'assistant' && !$isImage ) {
288 $url = $this->core->files->get_path( $newFileId );
289 $data = $this->core->files->get_data( $newFileId );
290 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
291 $filename = basename( $url );
292
293 // Upload the file
294 $file = $openai->upload_file( $filename, $data, 'assistants' );
295
296 // Create a store
297 if ( empty( $storeId ) ) {
298 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
299 if ( !empty( $query->chatId ) ) {
300 $chatbotName .= "_" . $query->chatId;
301 }
302 $expiry = $this->core->get_option( 'image_expires' );
303 $metadata = [];
304 if ( !empty( $chatbot['assistantId'] ) ) {
305 $metadata['assistantId'] = $chatbot['assistantId'];
306 }
307 if ( !empty( $query->chatId ) ) {
308 $metadata['chatId'] = $query->chatId;
309 }
310 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
311 $query->setStoreId( $storeId );
312 }
313
314 // Add the file to the store
315 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
316
317 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
318 $openAiRefId = $file['id'];
319 $internalFileId = $this->core->files->get_id_from_refId( $newFileId );
320 $this->core->files->update_refId( $internalFileId, $openAiRefId );
321 $this->core->files->update_envId( $internalFileId, $query->envId );
322 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
323 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
324 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
325 $newFileId = $openAiRefId;
326 $scope = $params['fileSearch'];
327 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
328 $id = $this->core->files->get_id_from_refId( $newFileId );
329 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
330 }
331 }
332 else {
333 $url = $this->core->files->get_url( $newFileId );
334 $mimeType = $this->core->files->get_mime_type( $newFileId );
335 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
336 $fileId = $this->core->files->get_id_from_refId( $newFileId );
337 $this->core->files->update_envId( $fileId, $query->envId );
338 $this->core->files->update_purpose( $fileId, 'vision' );
339 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
340 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
341 }
342 }
343
344 // Takeover
345 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
346 if ( !empty( $takeoverAnswer ) ) {
347 return [
348 'reply' => $takeoverAnswer,
349 'images' => null,
350 'usage' => null
351 ];
352 }
353
354 // Moderation
355 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
356 $this->core->get_option( 'shortcode_chat_moderation' );
357 if ( $moderationEnabled ) {
358 global $mwai;
359 $isFlagged = $mwai->moderationCheck( $query->get_message() );
360 if ( $isFlagged ) {
361 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
362 }
363 }
364
365 // Awareness & Embeddings
366 $context = $this->core->retrieve_context( $params, $query );
367 if ( !empty( $context ) ) {
368 $query->set_context( $context['content'] );
369 }
370
371 // Function Aware
372 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
373 }
374
375 // Process Query
376 if ( $stream ) {
377 $streamCallback = function( $reply ) {
378 $raw = $reply;
379 $this->stream_push( [ 'type' => 'live', 'data' => $raw ] );
380 if ( ob_get_level() > 0 ) {
381 ob_flush();
382 }
383 flush();
384 };
385 header( 'Cache-Control: no-cache' );
386 header( 'Content-Type: text/event-stream' );
387 // This is useful to disable buffering in nginx through headers.
388 header( 'X-Accel-Buffering: no' );
389 ob_implicit_flush( true );
390 ob_end_flush();
391 }
392
393 $reply = $this->core->run_query( $query, $streamCallback, true );
394 $rawText = $reply->result;
395 $extra = [];
396 if ( $context ) {
397 $extra = [ 'embeddings' => $context['embeddings'] ];
398 }
399 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
400
401 $actions = [];
402 if ( $reply->needClientActions ) {
403 foreach ( $reply->needClientActions as $action ) {
404 $actions[] = [
405 'type' => 'function',
406 'name' => $action['function']->name,
407 'args' => $action['arguments']
408 ];
409 }
410 }
411
412 $restRes = [
413 'reply' => $rawText,
414 'chatId' => $this->core->fix_chat_id( $query, $params ),
415 'images' => $reply->get_type() === 'images' ? $reply->results : null,
416 'actions' => $actions,
417 'usage' => $reply->usage
418 ];
419
420 // Process Reply
421 if ( $stream ) {
422 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
423 $restRes['reply'], $restRes['images'], $restRes['actions'], $restRes['usage'] );
424 $this->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ] );
425 die();
426 }
427 else {
428 return $restRes;
429 }
430
431 }
432 catch ( Exception $e ) {
433 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
434 if ( $stream ) {
435 $this->stream_push( [ 'type' => 'error', 'data' => $message ] );
436 die();
437 }
438 else {
439 throw $e;
440 }
441 }
442 }
443
444 public function stream_push( $data ) {
445 $out = "data: " . json_encode( $data );
446 echo $out;
447 echo "\n\n";
448 if (ob_get_level() > 0) {
449 ob_end_flush();
450 }
451 flush();
452 }
453
454 public function inject_chat() {
455 $params = $this->core->get_chatbot( $this->siteWideChatId );
456 $clean_params = [];
457 if ( !empty( $params ) ) {
458 $clean_params['window'] = true;
459 $clean_params['id'] = $this->siteWideChatId;
460 echo $this->chat_shortcode( $clean_params );
461 }
462 return null;
463 }
464
465 public function build_front_params( $botId, $customId ) {
466 $frontSystem = [
467 'botId' => $customId ? null : $botId,
468 'customId' => $customId,
469 'userData' => $this->core->get_user_data(),
470 'sessionId' => $this->core->get_session_id(),
471 'restNonce' => $this->core->get_nonce(),
472 'contextId' => get_the_ID(),
473 'pluginUrl' => MWAI_URL,
474 'restUrl' => untrailingslashit( get_rest_url() ),
475 'debugMode' => $this->core->get_option( 'debug_mode' ),
476 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
477 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
478 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
479 'stream' => $this->core->get_option( 'ai_streaming' ),
480 ];
481 return $frontSystem;
482 }
483
484 public function resolveBotInfo( &$atts )
485 {
486 $chatbot = null;
487 $botId = $atts['id'] ?? null;
488 $customId = $atts['custom_id'] ?? null;
489 if (!$botId && !$customId) {
490 $botId = "default";
491 }
492 if ( $botId ) {
493 $chatbot = $this->core->get_chatbot( $botId );
494 if (!$chatbot) {
495 $botId = $botId ?: 'N/A';
496 return [
497 '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'.",
498 ];
499 }
500 }
501 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
502 if ( !empty( $customId ) ) {
503 $botId = null;
504 }
505 unset( $atts['id'] );
506 return [
507 'chatbot' => $chatbot,
508 'botId' => $botId,
509 'customId' => $customId,
510 ];
511 }
512
513 // TODO: After January 2025, remove this.
514 public function old_chat_shortcode( $atts ) {
515 error_log( "AI Engine: The shortcode 'mwai_chatbot_v2' is deprecated. Please use 'mwai_chatbot' instead." );
516 return $this->chat_shortcode( $atts );
517 }
518
519 public function chat_shortcode( $atts ) {
520 $atts = empty( $atts ) ? [] : $atts;
521
522 // Let the user override the chatbot params
523 $atts = apply_filters( 'mwai_chatbot_params', $atts );
524
525 // Resolve the bot info
526 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
527 if ( isset( $resolvedBot['error'] ) ) {
528 return $resolvedBot['error'];
529 }
530 $chatbot = $resolvedBot['chatbot'];
531 $botId = $resolvedBot['botId'];
532 $customId = $resolvedBot['customId'];
533
534 // Rename the keys of the atts into camelCase to match the internal params system.
535 $atts = array_map( function( $key, $value ) {
536 $key = str_replace( '_', ' ', $key );
537 $key = ucwords( $key );
538 $key = str_replace( ' ', '', $key );
539 $key = lcfirst( $key );
540 return [ $key => $value ];
541 }, array_keys( $atts ), $atts );
542 $atts = array_merge( ...$atts );
543
544 $frontParams = [];
545 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
546 if ( isset( $atts[$param] ) ) {
547 if ( $param === 'localMemory' ) {
548 $frontParams[$param] = $atts[$param] === 'true';
549 }
550 else {
551 $frontParams[$param] = $atts[$param];
552 }
553 }
554 else if ( isset( $chatbot[$param] ) ) {
555 $frontParams[$param] = $chatbot[$param];
556 }
557 }
558
559 // Server Params
560 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
561 // we are using the default or a specific chatbot.
562 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
563 $serverParams = [];
564 if ( $hasServerOverrides ) {
565 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
566 if ( isset( $atts[$param] ) ) {
567 $serverParams[$param] = $atts[$param];
568 }
569 else {
570 $serverParams[$param] = $chatbot[$param] ?? null;
571 }
572 }
573 }
574
575 // Front Params
576 $frontSystem = $this->build_front_params( $botId, $customId );
577
578 // Clean Params
579 $frontParams = $this->clean_params( $frontParams );
580 $frontSystem = $this->clean_params( $frontSystem );
581 $serverParams = $this->clean_params( $serverParams );
582
583 // Server-side: Keep the System Params
584 if ( $hasServerOverrides ) {
585 if ( empty( $customId ) ) {
586 $customId = md5( json_encode( $serverParams ) );
587 $frontSystem['customId'] = $customId;
588 }
589 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
590 }
591
592 // Client-side: Prepare JSON for Front Params and System Params
593 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
594 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
595 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
596 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
597 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
598
599 $this->enqueue_scripts();
600 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
601 }
602
603 function chatbot_discussions( $atts ) {
604 $atts = empty($atts) ? [] : $atts;
605
606 // Resolve the bot info
607 $resolvedBot = $this->resolveBotInfo( $atts );
608 if ( isset( $resolvedBot['error'] ) ) {
609 return $resolvedBot['error'];
610 }
611 $chatbot = $resolvedBot['chatbot'];
612 $botId = $resolvedBot['botId'];
613 $customId = $resolvedBot['customId'];
614
615 // Rename the keys of the atts into camelCase to match the internal params system.
616 $atts = array_map( function( $key, $value ) {
617 $key = str_replace( '_', ' ', $key );
618 $key = ucwords( $key );
619 $key = str_replace( ' ', '', $key );
620 $key = lcfirst( $key );
621 return [ $key => $value ];
622 }, array_keys( $atts ), $atts );
623 $atts = array_merge( ...$atts );
624
625 // Front Params
626 $frontParams = [];
627 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
628 if ( isset( $atts[$param] ) ) {
629 $frontParams[$param] = $atts[$param];
630 }
631 else if ( isset( $chatbot[$param] ) ) {
632 $frontParams[$param] = $chatbot[$param];
633 }
634 }
635
636 // Server Params
637 $serverParams = [];
638 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
639 if ( isset( $atts[$param] ) ) {
640 $serverParams[$param] = $atts[$param];
641 }
642 }
643
644 // Front System
645 $frontSystem = $this->build_front_params( $botId, $customId );
646
647 // Clean Params
648 $frontParams = $this->clean_params( $frontParams );
649 $frontSystem = $this->clean_params( $frontSystem );
650 $serverParams = $this->clean_params( $serverParams );
651
652 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
653 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
654 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
655 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
656
657 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
658 }
659
660 function clean_params( &$params ) {
661 foreach ( $params as $param => $value ) {
662 if ( $param === 'restNonce' ) {
663 continue;
664 }
665 if ( empty( $value ) || is_array( $value ) ) {
666 continue;
667 }
668 $lowerCaseValue = strtolower( $value );
669 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
670 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
671 }
672 else if ( is_numeric( $value ) ) {
673 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
674 }
675 }
676 return $params;
677 }
678
679 }
680