PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 1.0.9
MxChat – AI Chatbot & Content Generation for WordPress v1.0.9
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.0.9, at includes/class-mxchat-integrator.php

569 lines 19.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 // Initialize the variable with the original message
196 $message_with_order_details = $message;
197
198 // Check if the user asked about orders
199 if (MxChat_WooCommerce::mxchat_is_order_related_query($message)) {
200 $order_details = MxChat_WooCommerce::mxchat_fetch_user_orders_details();
201
202 // If order details are available, append them to the user's message
203 if (!empty($order_details)) {
204 $message_with_order_details = $message . "\n\n" . $order_details;
205 }
206 }
207
208
209 // Save the combined message to the database
210 $this->mxchat_save_chat_message($session_id, 'user', $message_with_order_details);
211
212 // Generate and validate the embedding
213 $user_message_embedding = $this->mxchat_generate_embedding($message_with_order_details, $this->options['api_key']);
214 if (!is_array($user_message_embedding)) {
215 wp_send_json_error('Error processing your message.');
216 wp_die();
217 }
218
219 // Find relevant content based on embedding
220 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
221
222 // Fetch conversation history from the database
223 $conversation_history = $this->mxchat_fetch_conversation_history_for_ajax($session_id);
224
225 // Increment the chat count
226 $this->mxchat_increment_chat_count();
227
228 // Generate a response from the AI model
229 $response = $this->mxchat_generate_response($relevant_content, $this->options['api_key'], $conversation_history);
230
231 // Save the bot response to the database
232 $this->mxchat_save_chat_message($session_id, 'bot', $response);
233
234 // Send the response back to the client
235 wp_send_json(['message' => $response]);
236
237 wp_die();
238 }
239
240
241 private function mxchat_get_user_identifier() {
242 return MxChat_User::mxchat_get_user_identifier();
243 }
244
245
246
247 private function mxchat_generate_embedding($text, $api_key) {
248 $endpoint = 'https://api.openai.com/v1/embeddings';
249
250 $body = wp_json_encode([
251 'input' => $text,
252 'model' => 'text-embedding-ada-002'
253 ]);
254
255 $args = [
256 'body' => $body,
257 'headers' => [
258 'Content-Type' => 'application/json',
259 'Authorization' => 'Bearer ' . $api_key,
260 ],
261 'timeout' => 60,
262 'redirection' => 5,
263 'blocking' => true,
264 'httpversion' => '1.0',
265 'sslverify' => true,
266 ];
267
268 $response = wp_remote_post($endpoint, $args);
269
270 if (is_wp_error($response)) {
271 return null;
272 }
273
274 $response_body = json_decode(wp_remote_retrieve_body($response), true);
275
276 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
277 return $response_body['data'][0]['embedding'];
278 } else {
279 return null;
280 }
281 }
282
283 private function mxchat_find_relevant_content($user_embedding) {
284 global $wpdb;
285 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
286
287 // Define a cache key for embeddings
288 $cache_key = 'mxchat_system_prompt_embeddings';
289
290 // Attempt to get the embeddings from the cache
291 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
292
293 if ($embeddings === false) {
294 // Cache miss, query the database and cache the results
295 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
296 $embeddings = $wpdb->get_results($query);
297
298 if ($embeddings === null || empty($embeddings)) {
299 error_log("No embeddings found in the database.");
300 return null; // Return null to handle no embeddings gracefully
301 }
302
303 // Cache the results if successful
304 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600); // Cache for 1 hour
305 }
306
307 $most_relevant_id = null;
308 $highest_similarity = -INF;
309
310 foreach ($embeddings as $embedding) {
311 $database_embedding = maybe_unserialize($embedding->embedding_vector);
312
313 // Debugging: Log the embeddings
314 // if (!is_array($database_embedding)) {
315 // error_log("Invalid database embedding format for ID {$embedding->id}: " . print_r($database_embedding, true));
316 // continue;
317 // }
318
319 if (is_array($user_embedding)) {
320 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
321
322 // Debugging: Log the similarity score
323 // error_log("Calculated similarity for ID {$embedding->id}: {$similarity}");
324
325 if ($similarity > $highest_similarity) {
326 $highest_similarity = $similarity;
327 $most_relevant_id = $embedding->id;
328 }
329 } else {
330 // error_log("User embedding is not an array. Embedding data: " . print_r($user_embedding, true));
331 }
332 }
333
334 if ($most_relevant_id !== null) {
335 // Fetch content with product links
336 return $this->fetch_content_with_product_links($most_relevant_id);
337 }
338
339 error_log("No relevant content found. Most relevant ID was null.");
340 return null; // Return null if no relevant content is found
341 }
342
343
344 private function fetch_content_with_product_links($most_relevant_id) {
345 global $wpdb;
346 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
347
348 // Fetch the article content and associated product URL
349 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
350 $result = $wpdb->get_row($query);
351
352 if ($result) {
353 // Append the product link to the content if available
354 $content = $result->article_content;
355 if (!empty($result->source_url)) {
356 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
357 }
358 return $content;
359 }
360
361 return null;
362 }
363
364
365 private function mxchat_generate_response($relevant_content, $api_key, $conversation_history) {
366 if (!$relevant_content) {
367 return "I'm sorry, I couldn't find relevant information on that topic.";
368 }
369
370 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
371
372 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
373
374 array_unshift($conversation_history, [
375 'role' => 'system',
376 'content' => "Here are your instructions: " . $content_with_instructions
377 ]);
378
379 foreach ($conversation_history as &$message) {
380 if ($message['role'] === 'bot') {
381 $message['role'] = 'assistant';
382 }
383 }
384
385 $api_url = 'https://api.openai.com/v1/chat/completions';
386
387 $body = json_encode([
388 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-3.5',
389 'messages' => $conversation_history,
390 ]);
391
392 $args = [
393 'body' => $body,
394 'headers' => [
395 'Content-Type' => 'application/json',
396 'Authorization' => 'Bearer ' . $api_key,
397 ],
398 'timeout' => 60,
399 'redirection' => 5,
400 'blocking' => true,
401 'httpversion' => '1.0',
402 'sslverify' => true,
403 ];
404
405 $response = wp_remote_post($api_url, $args);
406
407 if (is_wp_error($response)) {
408 return "Sorry, there was an error processing your request.";
409 }
410
411 $response_body = json_decode(wp_remote_retrieve_body($response), true);
412
413 if (isset($response_body['choices'][0]['message']['content'])) {
414 if (isset($response_body['usage'])) {
415 $prompt_tokens = $response_body['usage']['prompt_tokens'];
416 $total_tokens = $response_body['usage']['total_tokens'];
417 }
418 return trim($response_body['choices'][0]['message']['content']);
419 } else {
420 return "Sorry, I couldn't process that request.";
421 }
422 }
423
424
425 public function mxchat_dismiss_pre_chat_message() {
426 // Get and sanitize the user identifier
427 $user_id = $this->mxchat_get_user_identifier();
428 $user_id = sanitize_key($user_id);
429
430 // Set a transient to track that the user has dismissed the pre-chat message
431 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
432 set_transient($transient_key, true, DAY_IN_SECONDS);
433
434 wp_send_json_success();
435 }
436
437
438
439 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
440 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
441 return 0;
442 }
443
444 $dotProduct = array_sum(array_map(function ($a, $b) {
445 return $a * $b;
446 }, $vectorA, $vectorB));
447 $normA = sqrt(array_sum(array_map(function ($a) {
448 return $a * $a;
449 }, $vectorA)));
450 $normB = sqrt(array_sum(array_map(function ($b) {
451 return $b * $b;
452 }, $vectorB)));
453
454 if ($normA == 0 || $normB == 0) {
455 return 0;
456 }
457
458 return $dotProduct / ($normA * $normB);
459 }
460
461 public function mxchat_enqueue_scripts_styles() {
462 // Define version numbers for the styles and scripts
463 $chat_style_version = '1.0.9'; // Replace with your actual version
464 $chat_script_version = '1.0.9'; // Replace with your actual version
465
466 // Correct path to the script file
467 wp_enqueue_script(
468 'mxchat-chat-js', // Handle for the script
469 plugin_dir_url(__FILE__) . '../js/chat-script.js', // Correct path using __FILE__
470 array('jquery'), // Dependencies
471 $chat_script_version, // Version for cache busting
472 true // Load script in footer
473 );
474
475 // Enqueue the CSS file similarly
476 wp_enqueue_style(
477 'mxchat-chat-css', // Handle for the style
478 plugin_dir_url(__FILE__) . '../css/chat-style.css', // Correct path using __FILE__
479 array(), // No dependencies
480 $chat_style_version // Version for cache busting
481 );
482
483 // Fetch options from the database
484 $this->options = get_option('mxchat_options');
485
486 // Prepare settings to pass to JavaScript
487 $style_settings = array(
488 'ajax_url' => admin_url('admin-ajax.php'),
489 'nonce' => wp_create_nonce('mxchat_chat_nonce'), // Nonce for security
490 'rate_limit_message' => 'Rate limit exceeded. Please try again later.',
491 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off'
492 );
493
494 // Localize the script with necessary data
495 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
496 }
497
498
499
500 public function mxchat_reset_rate_limits() {
501 global $wpdb;
502
503 // Define a cache key pattern for rate limits
504 $cache_key_pattern = 'mxchat_chat_limit_%';
505
506 // Retrieve all option names matching the pattern
507 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
508 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
509
510 // db call ok; no-cache ok
511 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
512 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
513
514 // Clear the relevant cache entries
515 foreach ($option_names as $option_name) {
516 wp_cache_delete($option_name, 'options');
517 }
518
519 // Optionally, clear a general cache if you have one
520 wp_cache_delete('mxchat_all_chat_limits', 'options');
521 }
522
523
524 private function mxchat_fetch_woocommerce_products() {
525 // Ensure WooCommerce is active
526 if (!class_exists('WooCommerce')) {
527 return [];
528 }
529
530 $args = array(
531 'post_type' => 'product',
532 'post_status' => 'publish',
533 'posts_per_page' => -1,
534 );
535
536 $products = get_posts($args);
537 $product_data = [];
538
539 foreach ($products as $product) {
540 $product_id = $product->ID;
541 $product_obj = wc_get_product($product_id);
542
543 $product_data[] = array(
544 'id' => $product_id,
545 'name' => $product_obj->get_name(),
546 'description' => $product_obj->get_description(),
547 'short_description' => $product_obj->get_short_description(),
548 'url' => get_permalink($product_id),
549 'price' => $product_obj->get_regular_price(),
550 'sale_price' => $product_obj->get_sale_price(),
551 'stock_status' => $product_obj->get_stock_status(),
552 'sku' => $product_obj->get_sku(),
553 'in_stock' => $product_obj->is_in_stock(),
554 'total_sales' => $product_obj->get_total_sales(),
555 );
556 }
557
558 return $product_data;
559 }
560
561
562
563
564
565
566
567 }
568 ?>
569