PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.4.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.4.9
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / modules / chatbot.php
ai-engine / classes / modules Last commit date
advisor.php 2 years ago chatbot.php 1 year ago discussions.php 1 year ago files.php 2 years ago security.php 2 years ago tasks.php 2 years ago wand.php 2 years ago
chatbot.php
690 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 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
128 $actions = $this->sanitize_actions( $actions );
129 $blocks = $this->sanitize_blocks( $blocks );
130 return [
131 'success' => true,
132 'reply' => $reply,
133 'images' => $images,
134 'actions' => $actions,
135 'shortcuts' => $shortcuts,
136 'blocks' => $blocks,
137 'usage' => $usage
138 ];
139 }
140
141 public function rest_chat( $request ) {
142 $params = $request->get_json_params();
143 $botId = $params['botId'] ?? null;
144 $customId = $params['customId'] ?? null;
145 $stream = $params['stream'] ?? false;
146 $newMessage = trim( $params['newMessage'] ?? '' );
147 $newFileId = $params['newFileId'] ?? null;
148
149 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId )) {
150 return new WP_REST_Response( [
151 'success' => false,
152 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
153 ], 403 );
154 }
155
156 try {
157 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream );
158 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
159 $data['reply'], $data['images'], $data['actions'], $data['usage'] );
160 return new WP_REST_Response( $final_res, 200 );
161 }
162 catch ( Exception $e ) {
163 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
164 return new WP_REST_Response( [
165 'success' => false,
166 'message' => $message
167 ], 500 );
168 }
169 }
170
171 private function sanitize_items( $items, $supported_types, $type_name ) {
172 $sanitized_items = [];
173 foreach ( $items as $item ) {
174 if ( isset( $supported_types[$item['type']] ) ) {
175 $is_valid = true;
176 foreach ( $supported_types[$item['type']] as $param ) {
177 if ( empty( $item['data'][$param] ) ) {
178 $is_valid = false;
179 $this->core->log( "⚠️ Missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
180 break;
181 }
182 }
183 if ( $is_valid ) {
184 $sanitized_items[] = $item;
185 }
186 }
187 else {
188 $this->core->log( "⚠️ Unsupported {$type_name} type: {$item['type']}." );
189 }
190 }
191 return $sanitized_items;
192 }
193
194 public function sanitize_actions( $actions ) {
195 $supported_action_types = [
196 'function' => ['name', 'args'],
197 'javascript' => ['snippet'],
198 ];
199 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
200 }
201
202 public function sanitize_blocks( $blocks ) {
203 $supported_block_types = [
204 'content' => ['html'],
205 ];
206 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
207 }
208
209 public function sanitize_shortcuts( $shortcuts ) {
210 $supported_shortcut_types = [
211 'message' => ['label', 'message'],
212 ];
213 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
214 }
215
216 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false ) {
217 try {
218 $chatbot = null;
219 $customId = $params['customId'] ?? null;
220
221 // Custom Chatbot
222 if ( $customId ) {
223 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
224 }
225 // Registered Chatbot
226 if ( !$chatbot && $botId ) {
227 $chatbot = $this->core->get_chatbot( $botId );
228 }
229
230 if ( !$chatbot ) {
231 $this->core->log( "⚠️ No chatbot was found for this query.");
232 throw new Exception( 'Sorry, your query has been rejected.' );
233 }
234
235 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
236 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int)$textInputMaxLength ) {
237 error_log( "AI Engine: The query was rejected - message was too long.");
238 throw new Exception( 'Sorry, your query has been rejected.' );
239 }
240
241 // Create QueryText
242 $context = null;
243 $mode = $chatbot['mode'] ?? 'chat';
244
245 if ( $mode === 'images' ) {
246 $query = new Meow_MWAI_Query_Image( $newMessage );
247
248 // Handle Params
249 $newParams = [];
250 foreach ( $chatbot as $key => $value ) {
251 $newParams[$key] = $value;
252 }
253 foreach ( $params as $key => $value ) {
254 $newParams[$key] = $value;
255 }
256 $params = apply_filters( 'mwai_chatbot_params', $newParams );
257 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
258 $query->inject_params( $params );
259 }
260 else {
261 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
262 new Meow_MWAI_Query_Text( $newMessage, 1024 );
263 $streamCallback = null;
264
265 // Handle Params
266 $newParams = [];
267 foreach ( $chatbot as $key => $value ) {
268 $newParams[$key] = $value;
269 }
270 foreach ( $params as $key => $value ) {
271 $newParams[$key] = $value;
272 }
273 $params = apply_filters( 'mwai_chatbot_params', $newParams );
274 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
275 $query->inject_params( $params );
276
277 $storeId = null;
278 if ( $mode === 'assistant' ) {
279 $chatId = $params['chatId'] ?? null;
280 if ( !empty( $chatId ) ) {
281 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
282 if ( isset( $discussion['storeId'] ) ) {
283 $storeId = $discussion['storeId'];
284 $query->setStoreId( $storeId );
285 }
286 }
287 }
288
289 // Support for Uploaded Image
290 if ( !empty( $newFileId ) ) {
291
292 // Get extension and mime type
293 $isImage = $this->core->files->is_image( $newFileId );
294
295 if ( $mode === 'assistant' && !$isImage ) {
296 $url = $this->core->files->get_path( $newFileId );
297 $data = $this->core->files->get_data( $newFileId );
298 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
299 $filename = basename( $url );
300
301 // Upload the file
302 $file = $openai->upload_file( $filename, $data, 'assistants' );
303
304 // Create a store
305 if ( empty( $storeId ) ) {
306 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
307 if ( !empty( $query->chatId ) ) {
308 $chatbotName .= "_" . $query->chatId;
309 }
310 $expiry = $this->core->get_option( 'image_expires' );
311 $metadata = [];
312 if ( !empty( $chatbot['assistantId'] ) ) {
313 $metadata['assistantId'] = $chatbot['assistantId'];
314 }
315 if ( !empty( $query->chatId ) ) {
316 $metadata['chatId'] = $query->chatId;
317 }
318 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
319 $query->setStoreId( $storeId );
320 }
321
322 // Add the file to the store
323 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
324
325 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
326 $openAiRefId = $file['id'];
327 $internalFileId = $this->core->files->get_id_from_refId( $newFileId );
328 $this->core->files->update_refId( $internalFileId, $openAiRefId );
329 $this->core->files->update_envId( $internalFileId, $query->envId );
330 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
331 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
332 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
333 $newFileId = $openAiRefId;
334 $scope = $params['fileSearch'];
335 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
336 $id = $this->core->files->get_id_from_refId( $newFileId );
337 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
338 }
339 }
340 else {
341 $url = $this->core->files->get_url( $newFileId );
342 $mimeType = $this->core->files->get_mime_type( $newFileId );
343 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
344 $fileId = $this->core->files->get_id_from_refId( $newFileId );
345 $this->core->files->update_envId( $fileId, $query->envId );
346 $this->core->files->update_purpose( $fileId, 'vision' );
347 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
348 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
349 }
350 }
351
352 // Takeover
353 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
354 if ( !empty( $takeoverAnswer ) ) {
355 return [
356 'reply' => $takeoverAnswer,
357 'images' => null,
358 'usage' => null
359 ];
360 }
361
362 // Moderation
363 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
364 $this->core->get_option( 'shortcode_chat_moderation' );
365 if ( $moderationEnabled ) {
366 global $mwai;
367 $isFlagged = $mwai->moderationCheck( $query->get_message() );
368 if ( $isFlagged ) {
369 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
370 }
371 }
372
373 // Awareness & Embeddings
374 $context = $this->core->retrieve_context( $params, $query );
375 if ( !empty( $context ) ) {
376 $query->set_context( $context['content'] );
377 }
378
379 // Function Aware
380 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
381 }
382
383 // Process Query
384 if ( $stream ) {
385 $streamCallback = function( $reply ) {
386 $raw = $reply;
387 $this->stream_push( [ 'type' => 'live', 'data' => $raw ] );
388 if ( ob_get_level() > 0 ) {
389 ob_flush();
390 }
391 flush();
392 };
393 header( 'Cache-Control: no-cache' );
394 header( 'Content-Type: text/event-stream' );
395 // This is useful to disable buffering in nginx through headers.
396 header( 'X-Accel-Buffering: no' );
397 ob_implicit_flush( true );
398 ob_end_flush();
399 }
400
401 $reply = $this->core->run_query( $query, $streamCallback, true );
402 $rawText = $reply->result;
403 $extra = [];
404 if ( $context ) {
405 $extra = [ 'embeddings' => $context['embeddings'] ];
406 }
407 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
408
409 $actions = [];
410 if ( $reply->needClientActions ) {
411 foreach ( $reply->needClientActions as $action ) {
412 $actions[] = [
413 'type' => 'function',
414 'data' => [
415 'name' => $action['function']->name,
416 'args' => $action['arguments']
417 ]
418 ];
419 }
420 }
421
422 $restRes = [
423 'reply' => $rawText,
424 'chatId' => $this->core->fix_chat_id( $query, $params ),
425 'images' => $reply->get_type() === 'images' ? $reply->results : null,
426 'actions' => $actions,
427 'usage' => $reply->usage
428 ];
429
430 // Process Reply
431 if ( $stream ) {
432 $final_res = $this->build_final_res( $botId, $newMessage, $newFileId, $params,
433 $restRes['reply'], $restRes['images'], $restRes['actions'], $restRes['usage'] );
434 $this->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ] );
435 die();
436 }
437 else {
438 return $restRes;
439 }
440
441 }
442 catch ( Exception $e ) {
443 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
444 if ( $stream ) {
445 $this->stream_push( [ 'type' => 'error', 'data' => $message ] );
446 die();
447 }
448 else {
449 throw $e;
450 }
451 }
452 }
453
454 public function stream_push( $data ) {
455 $out = "data: " . json_encode( $data );
456 echo $out;
457 echo "\n\n";
458 if (ob_get_level() > 0) {
459 ob_end_flush();
460 }
461 flush();
462 }
463
464 public function inject_chat() {
465 $params = $this->core->get_chatbot( $this->siteWideChatId );
466 $clean_params = [];
467 if ( !empty( $params ) ) {
468 $clean_params['window'] = true;
469 $clean_params['id'] = $this->siteWideChatId;
470 echo $this->chat_shortcode( $clean_params );
471 }
472 return null;
473 }
474
475 public function build_front_params( $botId, $customId ) {
476 $frontSystem = [
477 'botId' => $customId ? null : $botId,
478 'customId' => $customId,
479 'userData' => $this->core->get_user_data(),
480 'sessionId' => $this->core->get_session_id(),
481 'restNonce' => $this->core->get_nonce(),
482 'contextId' => get_the_ID(),
483 'pluginUrl' => MWAI_URL,
484 'restUrl' => untrailingslashit( get_rest_url() ),
485 'debugMode' => $this->core->get_option( 'debug_mode' ),
486 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
487 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
488 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
489 'stream' => $this->core->get_option( 'ai_streaming' ),
490 ];
491 return $frontSystem;
492 }
493
494 public function resolveBotInfo( &$atts )
495 {
496 $chatbot = null;
497 $botId = $atts['id'] ?? null;
498 $customId = $atts['custom_id'] ?? null;
499 if (!$botId && !$customId) {
500 $botId = "default";
501 }
502 if ( $botId ) {
503 $chatbot = $this->core->get_chatbot( $botId );
504 if (!$chatbot) {
505 $botId = $botId ?: 'N/A';
506 return [
507 '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'.",
508 ];
509 }
510 }
511 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
512 if ( !empty( $customId ) ) {
513 $botId = null;
514 }
515 unset( $atts['id'] );
516 return [
517 'chatbot' => $chatbot,
518 'botId' => $botId,
519 'customId' => $customId,
520 ];
521 }
522
523 // TODO: After January 2025, remove this.
524 public function old_chat_shortcode( $atts ) {
525 error_log( "AI Engine: The shortcode 'mwai_chatbot_v2' is deprecated. Please use 'mwai_chatbot' instead." );
526 return $this->chat_shortcode( $atts );
527 }
528
529 public function chat_shortcode( $atts ) {
530 $atts = empty( $atts ) ? [] : $atts;
531
532 // Let the user override the chatbot params
533 $atts = apply_filters( 'mwai_chatbot_params', $atts );
534
535 // Resolve the bot info
536 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
537 if ( isset( $resolvedBot['error'] ) ) {
538 return $resolvedBot['error'];
539 }
540 $chatbot = $resolvedBot['chatbot'];
541 $botId = $resolvedBot['botId'];
542 $customId = $resolvedBot['customId'];
543
544 // Rename the keys of the atts into camelCase to match the internal params system.
545 $atts = array_map( function( $key, $value ) {
546 $key = str_replace( '_', ' ', $key );
547 $key = ucwords( $key );
548 $key = str_replace( ' ', '', $key );
549 $key = lcfirst( $key );
550 return [ $key => $value ];
551 }, array_keys( $atts ), $atts );
552 $atts = array_merge( ...$atts );
553
554 $frontParams = [];
555 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
556 if ( isset( $atts[$param] ) ) {
557 if ( $param === 'localMemory' ) {
558 $frontParams[$param] = $atts[$param] === 'true';
559 }
560 else {
561 $frontParams[$param] = $atts[$param];
562 }
563 }
564 else if ( isset( $chatbot[$param] ) ) {
565 $frontParams[$param] = $chatbot[$param];
566 }
567 }
568
569 // Server Params
570 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
571 // we are using the default or a specific chatbot.
572 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
573 $serverParams = [];
574 if ( $hasServerOverrides ) {
575 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
576 if ( isset( $atts[$param] ) ) {
577 $serverParams[$param] = $atts[$param];
578 }
579 else {
580 $serverParams[$param] = $chatbot[$param] ?? null;
581 }
582 }
583 }
584
585 // Front Params
586 $frontSystem = $this->build_front_params( $botId, $customId );
587
588 // Clean Params
589 $frontParams = $this->clean_params( $frontParams );
590 $frontSystem = $this->clean_params( $frontSystem );
591 $serverParams = $this->clean_params( $serverParams );
592
593 // Server-side: Keep the System Params
594 if ( $hasServerOverrides ) {
595 if ( empty( $customId ) ) {
596 $customId = md5( json_encode( $serverParams ) );
597 $frontSystem['customId'] = $customId;
598 }
599 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
600 }
601
602 // Client-side: Prepare JSON for Front Params and System Params
603 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
604 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
605 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
606 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
607 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
608
609 $this->enqueue_scripts();
610 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
611 }
612
613 function chatbot_discussions( $atts ) {
614 $atts = empty($atts) ? [] : $atts;
615
616 // Resolve the bot info
617 $resolvedBot = $this->resolveBotInfo( $atts );
618 if ( isset( $resolvedBot['error'] ) ) {
619 return $resolvedBot['error'];
620 }
621 $chatbot = $resolvedBot['chatbot'];
622 $botId = $resolvedBot['botId'];
623 $customId = $resolvedBot['customId'];
624
625 // Rename the keys of the atts into camelCase to match the internal params system.
626 $atts = array_map( function( $key, $value ) {
627 $key = str_replace( '_', ' ', $key );
628 $key = ucwords( $key );
629 $key = str_replace( ' ', '', $key );
630 $key = lcfirst( $key );
631 return [ $key => $value ];
632 }, array_keys( $atts ), $atts );
633 $atts = array_merge( ...$atts );
634
635 // Front Params
636 $frontParams = [];
637 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
638 if ( isset( $atts[$param] ) ) {
639 $frontParams[$param] = $atts[$param];
640 }
641 else if ( isset( $chatbot[$param] ) ) {
642 $frontParams[$param] = $chatbot[$param];
643 }
644 }
645
646 // Server Params
647 $serverParams = [];
648 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
649 if ( isset( $atts[$param] ) ) {
650 $serverParams[$param] = $atts[$param];
651 }
652 }
653
654 // Front System
655 $frontSystem = $this->build_front_params( $botId, $customId );
656
657 // Clean Params
658 $frontParams = $this->clean_params( $frontParams );
659 $frontSystem = $this->clean_params( $frontSystem );
660 $serverParams = $this->clean_params( $serverParams );
661
662 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
663 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
664 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
665 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
666
667 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
668 }
669
670 function clean_params( &$params ) {
671 foreach ( $params as $param => $value ) {
672 if ( $param === 'restNonce' ) {
673 continue;
674 }
675 if ( empty( $value ) || is_array( $value ) ) {
676 continue;
677 }
678 $lowerCaseValue = strtolower( $value );
679 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
680 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
681 }
682 else if ( is_numeric( $value ) ) {
683 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
684 }
685 }
686 return $params;
687 }
688
689 }
690