options = get_option('mxchat_options');
$this->chat_count = get_option('mxchat_chat_count', 0);
// Add WooCommerce hooks
add_action('wp_insert_post', array($this, 'mxchat_handle_product_change'), 10, 3);
// Ensure embeddings are removed when a product is moved to trash or permanently deleted
add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
// Add the AJAX actions for checking if the pre-chat message was dismissed
add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
}
add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
}
public function mxchat_handle_product_change($post_id, $post, $update) {
// Ensure this is a product post type
if ($post->post_type !== 'product') {
return;
}
// Only generate embeddings if the product is published
if ($post->post_status === 'publish') {
// Delay the embedding slightly to ensure all product data is available
add_action('shutdown', function() use ($post_id) {
$product = wc_get_product($post_id);
if ($product && $product->get_price() !== '') {
$this->mxchat_store_product_embedding($product);
} else {
// Optionally, log or handle the case where product data is incomplete
// error_log("Product {$post_id} does not have complete data. Embedding not generated.");
}
});
}
}
public function mxchat_handle_product_delete($post_id) {
if (get_post_type($post_id) !== 'product') {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
// Delete the embedding associated with this product
$wpdb->delete($table_name, array('source_url' => get_permalink($post_id)), array('%s'));
}
private function mxchat_store_product_embedding($product) {
if (isset($this->options['enable_woocommerce_integration']) && $this->options['enable_woocommerce_integration'] === '1') {
$source_url = get_permalink($product->get_id());
$regular_price = $product->get_regular_price();
$sale_price = $product->get_sale_price();
$price = $sale_price ?: $regular_price;
$description = $product->get_description() . "\n\n" .
"Short Description: " . $product->get_short_description() . "\n" .
"Price: " . $regular_price . "\n" .
"Sale Price: " . ($sale_price ?: 'N/A') . "\n" .
"SKU: " . $product->get_sku();
global $wpdb;
$table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
// Delete any existing embedding for this product
$wpdb->delete($table_name, array('source_url' => $source_url), array('%s'));
// Submit the new content and embedding to the database
MxChat_Utils::submit_content_to_db($description, $source_url, $this->options['api_key']);
}
}
private function mxchat_increment_chat_count() {
$chat_count = get_option('mxchat_chat_count', 0);
$chat_count++;
update_option('mxchat_chat_count', $chat_count);
}
function mxchat_fetch_conversation_history() {
if (empty($_POST['session_id'])) {
wp_send_json_error(['message' => 'Session ID missing.']);
wp_die();
}
$session_id = sanitize_text_field($_POST['session_id']);
$history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
if (empty($history)) {
wp_send_json_error(['message' => 'No history found.']);
wp_die();
}
wp_send_json_success(['conversation' => $history]);
wp_die();
}
private function mxchat_fetch_conversation_history_for_ajax($session_id) {
$history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
$formatted_history = [];
// Format the history to align with the expected structure for OpenAI
foreach ($history as $entry) {
$formatted_history[] = [
'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
'content' => $entry['content']
];
}
return $formatted_history;
}
private function mxchat_fetch_conversation_history_for_ai($session_id) {
$history = get_option("mxchat_history_{$session_id}", []);
$formatted_history = [];
foreach ($history as $entry) {
// Skip messages containing HTML
if ($entry['content'] !== strip_tags($entry['content'])) {
continue;
}
$formatted_history[] = [
'role' => $entry['role'],
'content' => $entry['content']
];
}
return $formatted_history;
}
private function mxchat_save_chat_message($session_id, $role, $message) {
global $wpdb;
$table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
$user_id = is_user_logged_in() ? get_current_user_id() : 0;
$user_identifier = MxChat_User::mxchat_get_user_identifier();
$user_email = MxChat_User::mxchat_get_user_email();
$history = get_option("mxchat_history_{$session_id}", []);
$history[] = ['role' => $role, 'content' => $message];
update_option("mxchat_history_{$session_id}", $history);
$wpdb->insert($table_name, [
'user_id' => $user_id,
'user_identifier' => $user_identifier,
'user_email' => $user_email,
'session_id' => $session_id,
'role' => $role,
'message' => $message,
'timestamp' => current_time('mysql', 1)
]);
}
public function mxchat_handle_chat_request() {
global $wpdb;
// Get and sanitize the user identifier
$user_id = $this->mxchat_get_user_identifier();
$user_id = sanitize_key($user_id);
// Setup rate limiting
$rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
$chat_count = get_transient($rate_limit_transient_key) ?: 0;
// Retrieve the session ID from the client's POST data
$session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
if (empty($session_id)) {
wp_send_json_error('Session ID is missing.');
wp_die();
}
// Determine the rate limit based on login status
$rate_limit_option = is_user_logged_in()
? ($this->options['rate_limit_logged_in'] ?? '100')
: ($this->options['rate_limit_logged_out'] ?? '10');
if ($rate_limit_option !== 'unlimited' && $chat_count >= intval($rate_limit_option)) {
$rate_limit_message = $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.';
wp_send_json_error(['message' => $rate_limit_message]);
wp_die();
}
// Increment chat count only if limit is not 'unlimited'
if ($rate_limit_option !== 'unlimited') {
set_transient($rate_limit_transient_key, $chat_count + 1, DAY_IN_SECONDS);
}
// Validate and sanitize the incoming message
if (empty($_POST['message'])) {
wp_send_json_error('No message received.');
wp_die();
}
$message = wp_strip_all_tags($_POST['message'], false);
$message = trim($message);
$this->mxchat_save_chat_message($session_id, 'user', $message);
// Initialize response variables
$this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
$this->productCardHtml = '';
$intent_info = '';
// Step 1: Check for new PDF URL in the message
if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
$new_pdf_url = $matches[0];
// Validate HTTPS
if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
$existing_pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
// If a new URL is detected, reset all PDF-related transients
if ($existing_pdf_url !== $new_pdf_url) {
delete_transient('mxchat_pdf_url_' . $session_id);
delete_transient('mxchat_pdf_embeddings_' . $session_id);
delete_transient('mxchat_include_pdf_in_context_' . $session_id);
// Store the new PDF URL
set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
// Get the maximum number of pages allowed from admin settings
$max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Default to 69
// Fetch and process the new PDF
$embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages); // Pass both arguments
if ($embeddings === 'too_many_pages') {
// Handle PDF exceeding page limit
$error_text = sprintf(
$this->options['pdf_intent_error_text'] ?? "The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.",
$max_pages
);
$this->fallbackResponse['text'] = $error_text;
delete_transient('mxchat_pdf_url_' . $session_id);
} elseif ($embeddings) {
// Handle successful processing
set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
$success_text = $this->options['pdf_intent_success_text'] ?? "I've processed the new PDF. What questions do you have about it?";
$this->fallbackResponse['text'] = $success_text;
} else {
// Handle general processing error
$error_text = $this->options['pdf_intent_error_text'] ?? "Sorry, I couldn't process the new PDF. Please ensure it's a valid file.";
$this->fallbackResponse['text'] = $error_text;
delete_transient('mxchat_pdf_url_' . $session_id);
}
wp_send_json(['message' => $this->fallbackResponse['text']]);
wp_die();
}
}
}
// Step 2: Detect intent and handle intent-based responses
$intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
// Step 3: If intent is matched and handled, respond immediately
if (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html'])) {
$response_data = [
'text' => $this->fallbackResponse['text'],
'html' => $this->fallbackResponse['html'],
'session_id' => $session_id
];
$this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
wp_send_json($response_data);
wp_die();
}
// Step 4: Handle PDF-related content if applicable
$include_pdf_in_context = get_transient('mxchat_include_pdf_in_context_' . $session_id);
$pdf_vectors = get_transient('mxchat_pdf_embeddings_' . $session_id);
// Generate embedding for the user's query
$user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
if (!is_array($user_message_embedding)) {
wp_send_json_error('Error processing your message.');
wp_die();
}
// Prepare context for the response
$context_content = "User asked: '{$message}'\n\n";
// Retrieve the order summary if it exists (from the session-specific transient)
$orderSummary = get_transient('mxchat_order_summary_' . $session_id);
if ($orderSummary) {
// Include the order summary in the context for the AI
$context_content .= "\n" . $orderSummary . "\n";
// Optionally delete the transient if it should only be used once
// delete_transient('mxchat_order_summary_' . $session_id);
}
// Step 5: Fetch relevant content from the knowledge database
$relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
if (!empty($relevant_content)) {
$context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
}
// Step 6: Check PDF content if available
if ($include_pdf_in_context && is_array($pdf_vectors)) {
//error_log("Searching relevant PDF pages for query embedding.");
$relevant_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_vectors);
if (!empty($relevant_pages)) {
error_log("Relevant PDF pages found: " . implode(', ', array_map(function($page) {
return $page['page_number'];
}, $relevant_pages)));
foreach ($relevant_pages as $page_data) {
$context_content .= "Content from page {$page_data['page_number']}:\n" . $page_data['text'] . "\n\n";
}
} else {
//error_log("No relevant PDF pages found for query embedding.");
}
}
// Step 7: Generate AI response based on context and conversation history
$conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
$this->mxchat_increment_chat_count();
$response = $this->mxchat_generate_response(
$context_content,
$this->options['api_key'],
$this->options['xai_api_key'],
$this->options['claude_api_key'],
$conversation_history
);
$this->mxchat_save_chat_message($session_id, 'bot', $response);
// Step 8: Save and include additional content
if (!empty($this->productCardHtml)) {
$this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
}
if (!empty($this->fallbackResponse['html'])) {
$this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
}
// Step 9: Return the response
$response_data = [
'text' => $response,
'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
'session_id' => $session_id
];
wp_send_json($response_data);
wp_die();
}
// New function to check intents and invoke the callback function
private function mxchat_check_intent_and_invoke_callback( $message, $user_id, $session_id ) {
global $wpdb;
// Generate the user embedding
$user_embedding = $this->mxchat_generate_embedding( $message, $this->options['api_key'] );
if ( ! is_array( $user_embedding ) ) {
//error_log( 'mxchat: Error generating embedding for the message.' );
wp_send_json_error( 'Error generating embedding for the message.' );
return false;
}
// Fetch all intents from the database
$table_name = $wpdb->prefix . 'mxchat_intents';
$intents = $wpdb->get_results( "SELECT * FROM $table_name" );
if ( empty( $intents ) ) {
//error_log( 'mxchat: No intents found in the database.' );
return false; // No intents to process
}
$highest_similarity = -INF;
$matched_intent = null;
// Calculate similarity for each intent, using each intent's individual threshold if set
foreach ( $intents as $intent ) {
$intent_embedding_serialized = $intent->embedding_vector;
// Securely unserialize the embedding vector
$intent_embedding = $intent_embedding_serialized
? unserialize( $intent_embedding_serialized, [ 'allowed_classes' => false ] )
: null;
if ( ! is_array( $intent_embedding ) ) {
//error_log( "mxchat: Invalid embedding for intent '{$intent->intent_label}'. Skipping." );
continue; // Skip if the embedding is invalid
}
// Calculate cosine similarity between user message embedding and intent embedding
$similarity = $this->mxchat_calculate_cosine_similarity( $user_embedding, $intent_embedding );
//error_log( "mxchat: Similarity for intent '{$intent->intent_label}': {$similarity}" );
// Determine the similarity threshold for this intent
$intent_threshold = isset( $intent->similarity_threshold ) ? $intent->similarity_threshold : 0.85; // Default to 0.85 if not set
//error_log( "mxchat: Using threshold {$intent_threshold} for intent '{$intent->intent_label}'" );
// Only consider this intent as a potential match if it meets or exceeds the intent's threshold
if ( $similarity >= $intent_threshold && $similarity > $highest_similarity ) {
$highest_similarity = $similarity;
$matched_intent = $intent;
}
}
// Log the highest similarity and matched intent if found
if ( $matched_intent ) {
//error_log( "mxchat: Highest similarity is {$highest_similarity} for intent '{$matched_intent->intent_label}'" );
} else {
//error_log( 'mxchat: No intent matched.' );
}
// If a match is found above the threshold, call the associated callback
if ( $matched_intent ) {
//error_log( "mxchat: Intent '{$matched_intent->intent_label}' matches the threshold. Invoking callback '{$matched_intent->callback_function}'" );
// Ensure the callback function exists before calling it
if ( method_exists( $this, $matched_intent->callback_function ) ) {
// Call the intent handler function
call_user_func( [ $this, $matched_intent->callback_function ], $message, $user_id, $session_id );
return true; // Indicate that an intent was matched and handled
} else {
//error_log( "mxchat: Callback function '{$matched_intent->callback_function}' not found." );
}
} else {
//error_log( 'mxchat: No intent matched above its respective threshold.' );
}
return false; // No intent was matched above the threshold
}
//verified good
public function mxchat_handle_order_history($message, $user_id, $session_id) {
// Check if WooCommerce is active
if (!class_exists('WooCommerce')) {
$this->fallbackResponse['text'] = "Order functionality is currently unavailable.";
return;
}
// Determine if the user is asking for the "last order" or "all orders"
$queryType = (stripos($message, 'last order') !== false || stripos($message, 'recent order') !== false) ? 'last' : 'all';
// Fetch the relevant order details
$orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details($queryType);
if (empty($orderDetails)) {
$this->fallbackResponse['text'] = "No previous orders found.";
return;
}
// Prepare an order summary message
$orderSummary = ($queryType === 'last') ? "User's last order details:\n" : "User's recent order history:\n";
foreach ($orderDetails as $order) {
$items = implode(', ', $order['items']);
$orderSummary .= "Order #{$order['order_id']} placed on {$order['date']}, Status: {$order['status']}, Total: {$order['total']}, Items: {$items}\n";
}
// Store the order summary in a transient for the session
set_transient('mxchat_order_summary_' . $session_id, $orderSummary, HOUR_IN_SECONDS);
}
//verified good
public function mxchat_handle_product_inquiry( $message, $user_id, $session_id ) {
// Attempt to retrieve the product ID from the message
$product_id = MxChat_WooCommerce::mxchat_extract_product_id_from_message( $message );
// Check if there's a previously discussed product for follow-up queries
if ( ! $product_id ) {
$product_id = get_transient( 'mxchat_last_discussed_product_' . $user_id );
}
if ( $product_id && class_exists( 'WooCommerce' ) ) {
$product = wc_get_product( $product_id );
if ( $product ) {
// Escape and prepare product data
$product_name = esc_html( $product->get_name() );
$product_price = $product->get_price_html(); // Assuming this is safe HTML
$product_image_url = esc_url( wp_get_attachment_url( $product->get_image_id() ) );
$product_url = esc_url( get_permalink( $product_id ) );
$product_id_attr = esc_attr( $product_id );
// Generate the product card HTML using HEREDOC syntax
$product_card_html = <<
';
$this->fallbackResponse = [
'text' => "",
'html' => $html_output,
];
// Save response in chat history
$this->mxchat_save_chat_message($session_id, 'bot', $html_output);
} else {
/*
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
}
*/
$this->fallbackResponse = [
'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
'html' => "",
];
}
}
public function mxchat_interpret_search_query($user_query) {
$system_prompt = "Interpret the user's request to provide only the essential keywords or phrases for image searching. Remove conversational language, politeness, or extra context. Return a concise search query that doesn't lose any of the original meaning.";
// Retrieve OpenAI API key using 'api_key' as the option key
$api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
/*
// Log the API key check, without exposing the key
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
}
*/
if (empty($api_key)) {
//error_log("OpenAI API key is missing.");
return sanitize_text_field($user_query); // Default to the original query if API key is missing
}
$url = 'https://api.openai.com/v1/chat/completions';
$args = [
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'model' => 'gpt-3.5-turbo',
'messages' => [
['role' => 'system', 'content' => $system_prompt],
['role' => 'user', 'content' => sanitize_text_field($user_query)],
],
'temperature' => 0.2,
'max_tokens' => 20,
]),
'method' => 'POST',
];
$response = wp_remote_post($url, $args);
if (is_wp_error($response)) {
//error_log("OpenAI request failed: " . $response->get_error_message());
return sanitize_text_field($user_query); // Fallback to the original query if there's an error
}
$body = json_decode(wp_remote_retrieve_body($response), true);
// Check for a valid response and sanitize output
if (isset($body['choices'][0]['message']['content'])) {
$interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
/*
// Log the interpreted query for debugging
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log("Interpreted search query: " . $interpreted_query);
}
*/
return $interpreted_query;
} else {
//error_log("Unexpected API response format: " . print_r($body, true));
return sanitize_text_field($user_query);
}
}
//very good
public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) {
// Check if WooCommerce is active before proceeding
if (!class_exists('WooCommerce')) {
// error_log("WooCommerce is not active. Add-to-cart intent cannot be processed.");
return false;
}
// Sanitize user ID for transient key
$sanitized_user_id = sanitize_key($user_id);
// Retrieve the last discussed product ID from the transient
$last_product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
if ($last_product_id) {
// Attempt to add the product to the WooCommerce cart
$added = WC()->cart->add_to_cart($last_product_id);
$product = wc_get_product($last_product_id);
if ($added && $product) {
// Escape product name for safety
$product_name = esc_html($product->get_name());
// Success response if the product is added to the cart
$response = "The product '{$product_name}' has been added to your cart. To proceed to checkout, please type 'checkout'.";
set_transient('mxchat_checkout_prompt_' . $sanitized_user_id, true, 5 * MINUTE_IN_SECONDS);
// Save the response in chat history and send a JSON response
$this->mxchat_save_chat_message($session_id, 'bot', $response);
wp_send_json(['message' => $response]);
wp_die();
} else {
// Failure response if the product could not be added
$response = "Sorry, I couldn't add the product to your cart. Please try again.";
$this->mxchat_save_chat_message($session_id, 'bot', $response);
wp_send_json(['message' => $response]);
wp_die();
}
} else {
// Prompt the user to specify the product name if no product was found
$response = "I couldn't find the product to add. Please mention the product name again.";
$this->mxchat_save_chat_message($session_id, 'bot', $response);
wp_send_json(['message' => $response]);
wp_die();
}
}
public function mxchat_handle_checkout_intent($message, $user_id, $session_id) {
// Ensure WooCommerce is available and perform checkout actions
if (class_exists('WooCommerce')) {
// Sanitize user ID for transient key
$sanitized_user_id = sanitize_key($user_id);
$checkout_prompt = get_transient('mxchat_checkout_prompt_' . $sanitized_user_id);
// Check if there is an active checkout prompt and if the cart has items
if ($checkout_prompt && WC()->cart->get_cart_contents_count() > 0) {
// Get and escape the checkout URL
$checkout_url = wc_get_checkout_url();
$checkout_url = esc_url_raw($checkout_url);
$response = "Great! Redirecting you to the checkout page...";
// Clear the checkout prompt transient to avoid duplicate responses
delete_transient('mxchat_checkout_prompt_' . $sanitized_user_id);
// Save and send the response with the checkout URL for redirection
$this->mxchat_save_chat_message($session_id, 'bot', $response);
wp_send_json(['message' => $response, 'redirect_url' => $checkout_url]);
wp_die();
} else {
// If no active checkout prompt or empty cart, prompt user to add items first
$response = "It seems like there is no active checkout prompt or no items in your cart. Please add a product to the cart first.";
$this->mxchat_save_chat_message($session_id, 'bot', $response);
wp_send_json(['message' => $response]);
wp_die();
}
} else {
// WooCommerce is not available, send an error message
$response = "WooCommerce is not active on this site, so checkout is not possible.";
$this->mxchat_save_chat_message($session_id, 'bot', $response);
wp_send_json(['message' => $response]);
wp_die();
}
}
//very good
private function add_email_to_loops($email) {
// Sanitize the email
$email = sanitize_email($email);
// Retrieve and sanitize options
$api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
$mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
// Check for missing API key or mailing list ID
if (empty($api_key) || empty($mailing_list_id)) {
//error_log('Loops API key or mailing list ID is missing.');
return;
}
$data = array(
'email' => $email,
'subscribed' => true,
'source' => 'MxChat AI Chatbot',
'mailingLists' => array($mailing_list_id => true),
);
$url = 'https://app.loops.so/api/v1/contacts/create';
$args = array(
'body' => wp_json_encode($data),
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
),
'method' => 'POST',
'timeout' => 45,
);
$response = wp_remote_post($url, $args);
// Handle errors in the API request
if (is_wp_error($response)) {
//error_log('Error adding email to Loops: ' . $response->get_error_message());
return;
}
// Check for non-200 HTTP responses
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code != 200) {
$response_body = wp_remote_retrieve_body($response);
//error_log('Loops API responded with code ' . $response_code . ': ' . $response_body);
}
}
public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
// Get the maximum number of pages allowed from admin settings
$max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Default to 69
// Retrieve options for dynamic texts
$trigger_text = $this->options['pdf_intent_trigger_text'] ?? "Please provide the URL to the PDF you'd like to discuss.";
$success_text = $this->options['pdf_intent_success_text'] ?? "I've processed the PDF. What questions do you have about it?";
$error_text = $this->options['pdf_intent_error_text'] ?? "Sorry, I couldn't process the PDF. Please ensure it's a valid file.";
// Check if we're waiting for a URL
$waiting_for_url = get_transient('mxchat_waiting_for_pdf_url_' . $session_id);
// Check if we already have a PDF URL stored for this session
$pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
// Always process a new URL if detected in the current message
if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
$new_pdf_url = $matches[0];
// Validate HTTPS
if (!wp_http_validate_url($new_pdf_url) || parse_url($new_pdf_url, PHP_URL_SCHEME) !== 'https') {
//error_log("Invalid PDF URL: $new_pdf_url");
$this->fallbackResponse['text'] = $trigger_text;
return;
}
// Reset previous transients if a new URL is provided
if ($pdf_url !== $new_pdf_url) {
//error_log("New PDF URL detected. Resetting previous transients for session $session_id.");
delete_transient('mxchat_pdf_url_' . $session_id);
delete_transient('mxchat_pdf_embeddings_' . $session_id);
delete_transient('mxchat_include_pdf_in_context_' . $session_id);
// Store the new PDF URL
set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
// Fetch and process the new PDF
$embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages); // Ensure both arguments are passed
if ($embeddings === 'too_many_pages') {
//error_log("PDF exceeds maximum allowed pages ($max_pages) for URL: $new_pdf_url");
$error_text = sprintf(
$this->options['pdf_intent_error_text'] ?? "The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.",
$max_pages
);
$this->fallbackResponse['text'] = $error_text;
delete_transient('mxchat_pdf_url_' . $session_id);
} elseif ($embeddings) {
//error_log("PDF processed successfully for URL: $new_pdf_url");
set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
$this->fallbackResponse['text'] = $success_text;
} else {
//error_log("Failed to process PDF for URL: $new_pdf_url");
$this->fallbackResponse['text'] = $error_text;
delete_transient('mxchat_pdf_url_' . $session_id);
}
return;
}
}
if (!$pdf_url) {
//error_log("No PDF URL provided or stored for session $session_id.");
set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
$this->fallbackResponse['text'] = $trigger_text;
return;
}
// Retrieve stored embeddings or fetch if missing
$embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
if (!$embeddings) {
//error_log("No embeddings found for PDF URL: $pdf_url. Attempting to process again.");
$embeddings = $this->fetch_and_split_pdf_pages($pdf_url, $max_pages); // Ensure both arguments are passed
if ($embeddings === 'too_many_pages') {
//error_log("PDF exceeds maximum allowed pages ($max_pages) for URL: $pdf_url");
$this->fallbackResponse['text'] = "The provided PDF exceeds the maximum allowed limit of {$max_pages} pages. Please provide a smaller document.";
delete_transient('mxchat_pdf_url_' . $session_id);
} elseif ($embeddings) {
//error_log("PDF processed successfully for URL: $pdf_url");
set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
$this->fallbackResponse['text'] = $success_text;
} else {
//error_log("Failed to process PDF for URL: $pdf_url");
$this->fallbackResponse['text'] = $error_text;
delete_transient('mxchat_pdf_url_' . $session_id);
}
} else {
//error_log("Using stored embeddings for session $session_id.");
set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
$this->fallbackResponse['text'] = ''; // Proceed without additional message
}
}
private function fetch_and_split_pdf_pages($pdf_url, $max_pages) {
$upload_dir = wp_upload_dir();
$local_pdf_path = $upload_dir['path'] . '/' . basename(parse_url($pdf_url, PHP_URL_PATH));
$response = wp_remote_get($pdf_url, ['timeout' => 60]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
//error_log("Failed to download PDF. Error: " . print_r($response, true));
return false;
}
file_put_contents($local_pdf_path, wp_remote_retrieve_body($response));
try {
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile($local_pdf_path);
$pages = $pdf->getPages();
if (count($pages) > $max_pages) {
unlink($local_pdf_path);
return 'too_many_pages';
}
$embeddings = [];
foreach ($pages as $page_number => $page) {
$text = $page->getText();
$embedding = $this->mxchat_generate_embedding("Page " . ($page_number + 1) . ": " . $text, $this->options['api_key']);
if ($embedding) {
$embeddings[] = [
'page_number' => $page_number + 1,
'embedding' => $embedding,
'text' => $text,
];
} else {
//error_log("Failed to generate embedding for page " . ($page_number + 1));
}
}
unlink($local_pdf_path);
return $embeddings;
} catch (\Exception $e) {
unlink($local_pdf_path);
//error_log("Error parsing or processing PDF: " . $e->getMessage());
return false;
}
}
private function find_relevant_pdf_pages($query_embedding, $embeddings) {
//error_log("find_relevant_pdf_pages called.");
$most_relevant = null;
$highest_similarity = -INF;
foreach ($embeddings as $page_data) {
$similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
if ($similarity > $highest_similarity) {
$highest_similarity = $similarity;
$most_relevant = $page_data['page_number'];
}
}
if (!is_null($most_relevant)) {
$page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
return array_filter($embeddings, function ($page) use ($page_numbers) {
return in_array($page['page_number'], $page_numbers);
});
}
return [];
}
/**
* Calls the AI API with the provided prompt.
*
* @param string $prompt The prompt to send to the AI.
* @return array An array containing the AI's response text.
*/
private function mxchat_call_ai_api( $prompt ) {
//error_log( 'Calling AI API with the provided prompt.' );
$api_key = $this->options['api_key'];
if ( empty( $api_key ) ) {
//error_log( 'API key is not set.' );
return [ 'text' => 'API key is not set.' ];
}
$url = 'https://api.openai.com/v1/chat/completions';
$messages = [
[
'role' => 'system',
'content' => 'You are a helpful assistant that provides concise and personalized product recommendations. Someone has asked you for some recommendations please respond very concisely appropriate for an AI chatbot.',
],
[
'role' => 'user',
'content' => $prompt,
],
];
$args = [
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode(
[
'model' => 'gpt-4o',
'messages' => $messages,
'temperature' => 0.7,
]
),
'timeout' => 10,
'method' => 'POST',
];
$response = wp_remote_post( $url, $args );
if ( is_wp_error( $response ) ) {
//error_log( 'Error communicating with AI API: ' . $response->get_error_message() );
return [ 'text' => 'Error communicating with AI API.' ];
}
$body = wp_remote_retrieve_body( $response );
//error_log( 'API response body: ' . $body );
$decoded_body = json_decode( $body, true );
if ( isset( $decoded_body['choices'][0]['message']['content'] ) ) {
return [ 'text' => $decoded_body['choices'][0]['message']['content'] ];
} else {
//error_log( 'Unexpected API response format: ' . wp_json_encode( $decoded_body ) );
return [ 'text' => 'No response received from AI.' ];
}
}
/**
* Fetches the AI response for the given prompt.
*
* @param string $prompt The prompt to send to the AI.
* @return string|null The AI's response text or null if not available.
*/
private function mxchat_fetch_ai_response( $prompt ) {
$response = $this->mxchat_call_ai_api( $prompt );
return isset( $response['text'] ) ? $response['text'] : null;
}
/**
* Generates the AI prompt for product recommendations.
*
* @param array $recommendations An array of product recommendations.
* @return string The generated AI prompt.
*/
private function mxchat_generate_ai_recommendation_prompt( $recommendations ) {
//error_log( 'Generating AI recommendation prompt.' );
$recommendation_list = '';
foreach ( $recommendations as $index => $rec ) {
$number = $index + 1;
$name = $rec['name'];
$price = $rec['price'];
$url = $rec['url'];
$image = $rec['image'];
$recommendation_list .= "{$number}. Product: {$name} (Price: \${$price})\n";
$recommendation_list .= " [Link]({$url})\n";
$recommendation_list .= " \n\n";
}
$prompt = "Based on the following list of products, generate a unique, friendly, and personalized response to a user who asked for a recommendation ";
$prompt .= "Then, for each product, provide a brief justification of why it's relevant to the user. ";
$prompt .= "Please number your responses to match the product numbers.\n\n";
$prompt .= "Products:\n\n{$recommendation_list}";
$prompt .= "Please ensure that the number of each product matches the order in which the products are listed.";
//error_log( 'Generated AI prompt: ' . $prompt );
return $prompt;
}
/**
* Handles product recommendations by generating and formatting the AI response.
*
* @param string $message The user's message.
* @param int $user_id The user's ID.
* @param int $session_id The session ID.
*/
public function mxchat_handle_product_recommendations( $message, $user_id, $session_id ) {
try {
//error_log( "Starting product recommendations for user: $user_id, session: $session_id." );
$recommendation_data = $this->mxchat_generate_recommendations( $user_id );
//error_log( 'Generated recommendation data: ' . wp_json_encode( $recommendation_data ) );
if ( empty( $recommendation_data['recommendations'] ) ) {
//error_log( "No recommendations found for user: $user_id." );
$this->fallbackResponse = [
'text' => __( "I couldn't find any product recommendations for you right now. Please try again later!", 'mxchat' ),
];
return;
}
// Remove duplicates and limit to top 4 recommendations
$unique_recommendations = [];
foreach ( $recommendation_data['recommendations'] as $rec ) {
$unique_recommendations[ $rec['url'] ] = $rec;
}
//error_log( 'Unique recommendations: ' . wp_json_encode( $unique_recommendations ) );
$unique_recommendations = array_slice( $unique_recommendations, 0, 4 );
//error_log( 'Top 4 recommendations: ' . wp_json_encode( $unique_recommendations ) );
$recommendations_summary = [];
foreach ( $unique_recommendations as $rec ) {
$recommendations_summary[] = [
'name' => $rec['name'],
'price' => strip_tags( $rec['price'] ),
'url' => $rec['url'],
'image' => $rec['image'],
];
}
// Generate AI prompt and fetch response
$ai_prompt = $this->mxchat_generate_ai_recommendation_prompt( $recommendations_summary );
$ai_response = $this->mxchat_fetch_ai_response( $ai_prompt );
//error_log( 'AI response received: ' . wp_json_encode( $ai_response ) );
if ( empty( $ai_response ) ) {
$this->fallbackResponse = [
'text' => __( 'An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat' ),
];
return;
}
// Split the AI's response into lines
$ai_lines = preg_split( '/\r\n|\r|\n/', $ai_response );
// Initialize variables
$formatted_response = '';
$justifications = [];
$current_number = 0;
$in_introduction = true;
$introduction = '';
// Parse the AI response to separate the introduction and the justifications
foreach ( $ai_lines as $line ) {
if ( preg_match( '/^\s*(\d+)\.\s*(.*)$/', $line, $matches ) ) {
// This line is a numbered justification
$current_number = intval( $matches[1] ) - 1;
$justifications[ $current_number ] = $matches[2];
$in_introduction = false;
} elseif ( $in_introduction ) {
// This line is part of the introduction
$introduction .= $line . ' ';
} else {
// This line is a continuation of the current justification
if ( isset( $justifications[ $current_number ] ) ) {
$justifications[ $current_number ] .= ' ' . $line;
}
}
}
// Build the formatted response
if ( ! empty( $introduction ) ) {
$formatted_response .= esc_html( trim( $introduction ) ) . "