PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 1.1.2
MxChat – AI Chatbot & Content Generation for WordPress v1.1.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / includes / class-mxchat-integrator.php

class-mxchat-integrator.php in MxChat – AI Chatbot & Content Generation for WordPress 1.1.2, at includes/class-mxchat-integrator.php

630 lines 22.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 class MxChat_Integrator {
7 private $options;
8 private $chat_count;
9
10 public function __construct() {
11 $this->options = get_option('mxchat_options');
12 $this->chat_count = get_option('mxchat_chat_count', 0);
13
14 // Add WooCommerce hooks
15 add_action('wp_insert_post', array($this, 'mxchat_handle_product_change'), 10, 3);
16
17 // Ensure embeddings are removed when a product is moved to trash or permanently deleted
18 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
19 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
20
21 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
25 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26
27 if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
28 wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
29 }
30
31 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
32 }
33
34 public function mxchat_handle_product_change($post_id, $post, $update) {
35 // Ensure this is a product post type
36 if ($post->post_type !== 'product') {
37 return;
38 }
39
40 // Only generate embeddings if the product is published
41 if ($post->post_status === 'publish') {
42 // Delay the embedding slightly to ensure all product data is available
43 add_action('shutdown', function() use ($post_id) {
44 $product = wc_get_product($post_id);
45 if ($product && $product->get_price() !== '') {
46 $this->mxchat_store_product_embedding($product);
47 } else {
48 // Optionally, log or handle the case where product data is incomplete
49 error_log("Product {$post_id} does not have complete data. Embedding not generated.");
50 }
51 });
52 }
53 }
54
55 public function mxchat_handle_product_delete($post_id) {
56 if (get_post_type($post_id) !== 'product') {
57 return;
58 }
59
60 global $wpdb;
61 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
62
63 // Delete the embedding associated with this product
64 $wpdb->delete($table_name, array('source_url' => get_permalink($post_id)), array('%s'));
65 }
66
67 private function mxchat_store_product_embedding($product) {
68 if (isset($this->options['enable_woocommerce_integration']) && $this->options['enable_woocommerce_integration'] === '1') {
69
70 $source_url = get_permalink($product->get_id());
71 $regular_price = $product->get_regular_price();
72 $sale_price = $product->get_sale_price();
73 $price = $sale_price ?: $regular_price;
74
75 $description = $product->get_description() . "\n\n" .
76 "Short Description: " . $product->get_short_description() . "\n" .
77 "Price: " . $regular_price . "\n" .
78 "Sale Price: " . ($sale_price ?: 'N/A') . "\n" .
79 "SKU: " . $product->get_sku();
80
81 global $wpdb;
82 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
83
84 // Delete any existing embedding for this product
85 $wpdb->delete($table_name, array('source_url' => $source_url), array('%s'));
86
87 // Submit the new content and embedding to the database
88 MxChat_Utils::submit_content_to_db($description, $source_url, $this->options['api_key']);
89 }
90 }
91
92
93
94
95
96 private function mxchat_increment_chat_count() {
97 $chat_count = get_option('mxchat_chat_count', 0);
98 $chat_count++;
99 update_option('mxchat_chat_count', $chat_count);
100 }
101
102 public function mxchat_fetch_conversation_history_for_ajax($session_id) {
103 global $wpdb;
104 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
105
106 // Prepare and execute the query safely
107 $chat_transcripts = $wpdb->get_results(
108 $wpdb->prepare("SELECT * FROM $table_name WHERE session_id = %s ORDER BY timestamp ASC", sanitize_text_field($session_id))
109 );
110
111 // Check if results are empty
112 if (empty($chat_transcripts)) {
113 return [];
114 }
115
116 // Build the conversation history
117 $conversation_history = [];
118 foreach ($chat_transcripts as $transcript) {
119 $conversation_history[] = [
120 'role' => $transcript->role,
121 'content' => $transcript->message
122 ];
123 }
124
125 return $conversation_history;
126 }
127
128
129 private function mxchat_save_chat_message($session_id, $role, $message) {
130 global $wpdb;
131 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
132
133 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
134 $user_identifier = MxChat_User::mxchat_get_user_identifier();
135 $user_email = MxChat_User::mxchat_get_user_email();
136
137 $wpdb->insert($table_name, [
138 'user_id' => $user_id,
139 'user_identifier' => $user_identifier,
140 'user_email' => $user_email,
141 'session_id' => $session_id,
142 'role' => $role,
143 'message' => $message,
144 'timestamp' => current_time('mysql', 1)
145 ]);
146 }
147
148 public function mxchat_handle_chat_request() {
149 global $wpdb;
150
151 // Get and sanitize the user identifier
152 $user_id = $this->mxchat_get_user_identifier();
153 $user_id = sanitize_key($user_id);
154
155 // Manage rate limiting
156 $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
157 $chat_count = get_transient($rate_limit_transient_key);
158 $session_transient_key = 'mxchat_chat_session_' . $user_id;
159 $session_id = get_transient($session_transient_key);
160
161 if ($chat_count === false) {
162 $chat_count = 0;
163 }
164
165 if ($session_id === false) {
166 $session_id = uniqid('mxchat_chat_', true);
167 set_transient($session_transient_key, $session_id, DAY_IN_SECONDS); // Store session ID for a day
168 }
169
170 $rate_limit_option = isset($this->options['rate_limit']) ? $this->options['rate_limit'] : 'unlimited';
171
172 // Check if rate limit is not 'unlimited'
173 if ($rate_limit_option !== 'unlimited') {
174 $rate_limit = intval($rate_limit_option);
175
176 if ($chat_count >= $rate_limit) {
177 wp_send_json_error('Rate limit exceeded. Please try again later.');
178 wp_die();
179 }
180 set_transient($rate_limit_transient_key, $chat_count + 1, DAY_IN_SECONDS);
181 }
182
183 // Validate and sanitize the incoming message
184 if (!isset($_POST['message'])) {
185 wp_send_json_error('No message received');
186 wp_die();
187 }
188
189 $message = sanitize_text_field($_POST['message']);
190 if (empty($message)) {
191 wp_send_json_error('Message is empty or invalid.');
192 wp_die();
193 }
194
195 // Check if WooCommerce integration is enabled and WooCommerce functions are available
196 if (MxChat_WooCommerce::is_order_access_enabled() && function_exists('WC')) {
197 // Ensure WooCommerce session and cart are initialized
198 if (!WC()->session) {
199 WC()->session = new WC_Session_Handler();
200 WC()->session->init();
201 }
202 if (!WC()->cart) {
203 WC()->cart = new WC_Cart();
204 WC()->cart->init();
205 }
206
207 // Handle the "add to cart" request
208 if (stripos($message, 'add to cart') !== false) {
209 $last_product_id = get_transient('mxchat_last_discussed_product_' . $user_id);
210
211 if ($last_product_id) {
212 $added = WC()->cart->add_to_cart($last_product_id);
213 $product = wc_get_product($last_product_id);
214
215 if ($added) {
216 $response = "The product '{$product->get_name()}' has been added to your cart. To proceed to checkout, please type 'checkout'.";
217 set_transient('mxchat_checkout_prompt_' . $user_id, true, 5 * MINUTE_IN_SECONDS); // Valid for 5 minutes
218
219 // Save the bot response to the database
220 $this->mxchat_save_chat_message($session_id, 'bot', $response);
221
222 // Send the response back to the client
223 wp_send_json(['message' => $response]);
224 wp_die();
225 } else {
226 $response = "Sorry, I couldn't add the product to your cart. Please try again.";
227 $this->mxchat_save_chat_message($session_id, 'bot', $response);
228 wp_send_json(['message' => $response]);
229 wp_die();
230 }
231 } else {
232 $response = "I couldn't find the product to add. Please mention the product name again.";
233 $this->mxchat_save_chat_message($session_id, 'bot', $response);
234 wp_send_json(['message' => $response]);
235 wp_die();
236 }
237 }
238
239 // Handle the "checkout" response
240 if (stripos($message, 'checkout') !== false) {
241 $checkout_prompt = get_transient('mxchat_checkout_prompt_' . $user_id);
242
243 if ($checkout_prompt && WC()->cart && WC()->cart->get_cart_contents_count() > 0) {
244 $checkout_url = wc_get_checkout_url();
245 $response = "Great! Redirecting you to the checkout page...";
246 delete_transient('mxchat_checkout_prompt_' . $user_id);
247
248 // Save the bot response to the database
249 $this->mxchat_save_chat_message($session_id, 'bot', $response);
250
251 // Send the response and checkout URL
252 wp_send_json(['message' => $response, 'redirect_url' => $checkout_url]);
253 wp_die();
254 } else {
255 $response = "It seems like there is no active checkout prompt or no items in your cart. Please add a product to the cart first.";
256 $this->mxchat_save_chat_message($session_id, 'bot', $response);
257 wp_send_json(['message' => $response]);
258 wp_die();
259 }
260 }
261 }
262
263 // Store the last discussed product in transient if applicable
264 $last_discussed_product_id = MxChat_WooCommerce::mxchat_extract_product_id_from_message($message);
265 if ($last_discussed_product_id) {
266 set_transient('mxchat_last_discussed_product_' . $user_id, $last_discussed_product_id, 3600); // Cache for 1 hour
267 }
268
269 // Save the user's message to the database once here
270 $this->mxchat_save_chat_message($session_id, 'user', $message);
271
272 // Generate and validate the embedding
273 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
274 if (!is_array($user_message_embedding)) {
275 wp_send_json_error('Error processing your message.');
276 wp_die();
277 }
278
279 // Find relevant content based on embedding
280 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
281
282 // Fetch conversation history from the database
283 $conversation_history = $this->mxchat_fetch_conversation_history_for_ajax($session_id);
284
285 // Increment the chat count
286 $this->mxchat_increment_chat_count();
287
288 // Generate a response from the AI model
289 $response = $this->mxchat_generate_response($relevant_content, $this->options['api_key'], $conversation_history);
290
291 // Save the bot response to the database
292 $this->mxchat_save_chat_message($session_id, 'bot', $response);
293
294 // Send the response back to the client
295 wp_send_json(['message' => $response]);
296
297 wp_die();
298 }
299
300
301
302 private function mxchat_get_user_identifier() {
303 return MxChat_User::mxchat_get_user_identifier();
304 }
305
306
307
308 private function mxchat_generate_embedding($text, $api_key) {
309 $endpoint = 'https://api.openai.com/v1/embeddings';
310
311 $body = wp_json_encode([
312 'input' => $text,
313 'model' => 'text-embedding-ada-002'
314 ]);
315
316 $args = [
317 'body' => $body,
318 'headers' => [
319 'Content-Type' => 'application/json',
320 'Authorization' => 'Bearer ' . $api_key,
321 ],
322 'timeout' => 60,
323 'redirection' => 5,
324 'blocking' => true,
325 'httpversion' => '1.0',
326 'sslverify' => true,
327 ];
328
329 $response = wp_remote_post($endpoint, $args);
330
331 if (is_wp_error($response)) {
332 return null;
333 }
334
335 $response_body = json_decode(wp_remote_retrieve_body($response), true);
336
337 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
338 return $response_body['data'][0]['embedding'];
339 } else {
340 return null;
341 }
342 }
343
344 private function mxchat_find_relevant_content($user_embedding) {
345 global $wpdb;
346 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
347
348 // Define a cache key for embeddings
349 $cache_key = 'mxchat_system_prompt_embeddings';
350
351 // Attempt to get the embeddings from the cache
352 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
353
354 if ($embeddings === false) {
355 // Cache miss, query the database and cache the results
356 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
357 $embeddings = $wpdb->get_results($query);
358
359 if ($embeddings === null || empty($embeddings)) {
360 error_log("No embeddings found in the database.");
361 return null; // Return null to handle no embeddings gracefully
362 }
363
364 // Cache the results if successful
365 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600); // Cache for 1 hour
366 }
367
368 $most_relevant_id = null;
369 $highest_similarity = -INF;
370
371 foreach ($embeddings as $embedding) {
372 $database_embedding = maybe_unserialize($embedding->embedding_vector);
373
374 // Debugging: Log the embeddings
375 // if (!is_array($database_embedding)) {
376 // error_log("Invalid database embedding format for ID {$embedding->id}: " . print_r($database_embedding, true));
377 // continue;
378 // }
379
380 if (is_array($user_embedding)) {
381 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
382
383 // Debugging: Log the similarity score
384 // error_log("Calculated similarity for ID {$embedding->id}: {$similarity}");
385
386 if ($similarity > $highest_similarity) {
387 $highest_similarity = $similarity;
388 $most_relevant_id = $embedding->id;
389 }
390 } else {
391 // error_log("User embedding is not an array. Embedding data: " . print_r($user_embedding, true));
392 }
393 }
394
395 if ($most_relevant_id !== null) {
396 // Fetch content with product links
397 return $this->fetch_content_with_product_links($most_relevant_id);
398 }
399
400 error_log("No relevant content found. Most relevant ID was null.");
401 return null; // Return null if no relevant content is found
402 }
403
404
405 private function fetch_content_with_product_links($most_relevant_id) {
406 global $wpdb;
407 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
408
409 // Fetch the article content and associated product URL
410 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
411 $result = $wpdb->get_row($query);
412
413 if ($result) {
414 // Append the product link to the content if available
415 $content = $result->article_content;
416 if (!empty($result->source_url)) {
417 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
418 }
419 return $content;
420 }
421
422 return null;
423 }
424
425
426 private function mxchat_generate_response($relevant_content, $api_key, $conversation_history) {
427 if (!$relevant_content) {
428 return "I'm sorry, I couldn't find relevant information on that topic.";
429 }
430
431 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
432
433 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
434
435 array_unshift($conversation_history, [
436 'role' => 'system',
437 'content' => "Here are your instructions: " . $content_with_instructions
438 ]);
439
440 foreach ($conversation_history as &$message) {
441 if ($message['role'] === 'bot') {
442 $message['role'] = 'assistant';
443 }
444 }
445
446 $api_url = 'https://api.openai.com/v1/chat/completions';
447
448 $body = json_encode([
449 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-3.5',
450 'messages' => $conversation_history,
451 ]);
452
453 $args = [
454 'body' => $body,
455 'headers' => [
456 'Content-Type' => 'application/json',
457 'Authorization' => 'Bearer ' . $api_key,
458 ],
459 'timeout' => 60,
460 'redirection' => 5,
461 'blocking' => true,
462 'httpversion' => '1.0',
463 'sslverify' => true,
464 ];
465
466 $response = wp_remote_post($api_url, $args);
467
468 if (is_wp_error($response)) {
469 return "Sorry, there was an error processing your request.";
470 }
471
472 $response_body = json_decode(wp_remote_retrieve_body($response), true);
473
474 if (isset($response_body['choices'][0]['message']['content'])) {
475 if (isset($response_body['usage'])) {
476 $prompt_tokens = $response_body['usage']['prompt_tokens'];
477 $total_tokens = $response_body['usage']['total_tokens'];
478 }
479 return trim($response_body['choices'][0]['message']['content']);
480 } else {
481 return "Sorry, I couldn't process that request.";
482 }
483 }
484
485
486 public function mxchat_dismiss_pre_chat_message() {
487 // Get and sanitize the user identifier
488 $user_id = $this->mxchat_get_user_identifier();
489 $user_id = sanitize_key($user_id);
490
491 // Set a transient to track that the user has dismissed the pre-chat message
492 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
493 set_transient($transient_key, true, DAY_IN_SECONDS);
494
495 wp_send_json_success();
496 }
497
498
499
500 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
501 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
502 return 0;
503 }
504
505 $dotProduct = array_sum(array_map(function ($a, $b) {
506 return $a * $b;
507 }, $vectorA, $vectorB));
508 $normA = sqrt(array_sum(array_map(function ($a) {
509 return $a * $a;
510 }, $vectorA)));
511 $normB = sqrt(array_sum(array_map(function ($b) {
512 return $b * $b;
513 }, $vectorB)));
514
515 if ($normA == 0 || $normB == 0) {
516 return 0;
517 }
518
519 return $dotProduct / ($normA * $normB);
520 }
521
522 public function mxchat_enqueue_scripts_styles() {
523 // Define version numbers for the styles and scripts
524 $chat_style_version = '1.1.2'; // Replace with your actual version
525 $chat_script_version = '1.1.2'; // Replace with your actual version
526
527 // Correct path to the script file
528 wp_enqueue_script(
529 'mxchat-chat-js', // Handle for the script
530 plugin_dir_url(__FILE__) . '../js/chat-script.js', // Correct path using __FILE__
531 array('jquery'), // Dependencies
532 $chat_script_version, // Version for cache busting
533 true // Load script in footer
534 );
535
536 // Enqueue the CSS file similarly
537 wp_enqueue_style(
538 'mxchat-chat-css', // Handle for the style
539 plugin_dir_url(__FILE__) . '../css/chat-style.css', // Correct path using __FILE__
540 array(), // No dependencies
541 $chat_style_version // Version for cache busting
542 );
543
544 // Fetch options from the database
545 $this->options = get_option('mxchat_options');
546
547 // Prepare settings to pass to JavaScript
548 $style_settings = array(
549 'ajax_url' => admin_url('admin-ajax.php'),
550 'nonce' => wp_create_nonce('mxchat_chat_nonce'), // Nonce for security
551 'rate_limit_message' => 'Rate limit exceeded. Please try again later.',
552 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off'
553 );
554
555 // Localize the script with necessary data
556 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
557 }
558
559
560
561 public function mxchat_reset_rate_limits() {
562 global $wpdb;
563
564 // Define a cache key pattern for rate limits
565 $cache_key_pattern = 'mxchat_chat_limit_%';
566
567 // Retrieve all option names matching the pattern
568 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
569 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
570
571 // db call ok; no-cache ok
572 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
573 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
574
575 // Clear the relevant cache entries
576 foreach ($option_names as $option_name) {
577 wp_cache_delete($option_name, 'options');
578 }
579
580 // Optionally, clear a general cache if you have one
581 wp_cache_delete('mxchat_all_chat_limits', 'options');
582 }
583
584
585 private function mxchat_fetch_woocommerce_products() {
586 // Ensure WooCommerce is active
587 if (!class_exists('WooCommerce')) {
588 return [];
589 }
590
591 $args = array(
592 'post_type' => 'product',
593 'post_status' => 'publish',
594 'posts_per_page' => -1,
595 );
596
597 $products = get_posts($args);
598 $product_data = [];
599
600 foreach ($products as $product) {
601 $product_id = $product->ID;
602 $product_obj = wc_get_product($product_id);
603
604 $product_data[] = array(
605 'id' => $product_id,
606 'name' => $product_obj->get_name(),
607 'description' => $product_obj->get_description(),
608 'short_description' => $product_obj->get_short_description(),
609 'url' => get_permalink($product_id),
610 'price' => $product_obj->get_regular_price(),
611 'sale_price' => $product_obj->get_sale_price(),
612 'stock_status' => $product_obj->get_stock_status(),
613 'sku' => $product_obj->get_sku(),
614 'in_stock' => $product_obj->is_in_stock(),
615 'total_sales' => $product_obj->get_total_sales(),
616 );
617 }
618
619 return $product_data;
620 }
621
622
623
624
625
626
627
628 }
629 ?>
630