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

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