prefix . 'rl_mailwarmer_conversations'; // $current_time = current_time('mysql'); $start_time = date('Y-m-d H:i:s', strtotime('-96 hours')); $end_time = date('Y-m-d H:i:s', strtotime('+2 hours')); // $end_time = date('Y-m-d H:i:s', strtotime('now')); // Fetch up to 50 conversations starting within the next 24 hours $conversations = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $conversation_table WHERE status = %s AND first_message_timestamp BETWEEN %s AND %s ORDER BY first_message_timestamp ASC LIMIT 5", 'new', $start_time, $end_time ), ARRAY_A ); $conversations_count = count($conversations); if ($conversations_count > 0) { log_to_file("process_upcoming_conversations - Processing {$conversations_count} conversations" ); action_log("process_upcoming_conversations - Processing {$conversations_count} conversations" ); foreach ($conversations as $conversation) { log_to_file("process_upcoming_conversations - Conversation: ", $conversation['id']); try { // Generate conversation content using AI // $ai_response = self::fetch_conversation_from_ai($conversation); $ai_response = isset($conversation['ai_response']) ? json_decode($conversation['ai_response'], true) : false; $prompt = $conversation['prompt']; $model = $conversation['model']; // log_to_file("process_upcoming_conversations - AI prompt: ", $prompt); // $ai_response = RL_MailWarmer_Campaign_Helper::clean_ai_response(RL_MailWarmer_Campaign_Helper::generate_ai_conversation($prompt)); if ($ai_response) { log_to_file("process_upcoming_conversations - Using cached AI Response"); } else { log_to_file("process_upcoming_conversations - No cached AI Response found. Fetching."); $raw_ai_response = self::generate_ai_conversation($prompt, $model); // log_to_file("process_upcoming_conversations - AI Reponse: {$raw_ai_response}"); // $ai_response_type = gettype($raw_ai_response); // log_to_file("process_upcoming_conversations - AI Reponse is a : ", $ai_response_type); $ai_response = self::normalize_conversation_format($raw_ai_response); // Save the generated content to the conversation $update_data = [ 'ai_response' => json_encode($ai_response), ]; $update_db_with_AI_response = RL_MailWarmer_DB_Helper::update_conversation(intval($conversation['id']), $update_data); // log_to_file("process_upcoming_conversations - update_db_with_AI_response: ", $update_db_with_AI_response); } // $ai_response = get_post_meta($conversation['campaign_id'], 'last_ai_response', true); log_to_file("process_upcoming_conversations - AI Response: ", $ai_response); if (is_wp_error($ai_response)) { log_to_file("process_upcoming_conversations - AI Response is an error!"); return $ai_response; // Handle AI generation failure gracefully } log_to_file("process_upcoming_conversations - Before merge_timeline_with_ai_response"); $updated_conversation_steps = self::merge_timeline_with_ai_response($conversation['conversation_steps'], $ai_response); log_to_file("process_upcoming_conversations - Merged steps: ", $updated_conversation_steps); // // Update conversation status to "generated" $conversation_table = $wpdb->prefix . 'rl_mailwarmer_conversations'; $status = 'generated'; // log_to_file("process_upcoming_conversations - Updating DB for conversation {$conversation['id']} to status: {$status}. Response: {$ai_response} New steps: ", $updated_conversation_steps); $result = $wpdb->update( $conversation_table, // [], ['status' => $status, 'conversation_steps' => json_encode($updated_conversation_steps), 'ai_response' => json_encode($ai_response)], ['id' => $conversation['id']], ['%s'], ['%s'], ['%s'], ['%d'] ); // log_to_file("process_upcoming_conversations - Update DB Result: ", $result); $update_messages_db = self::update_messages_with_merged_output($conversation['id'], json_encode($updated_conversation_steps)); log_to_file("process_upcoming_conversations - Result of update_messages_with_merged_output(): ", $update_messages_db); // Update the conversation with generated steps log_to_file("process_upcoming_conversations - Updating conversation_steps"); if (self::update_conversation_steps($conversation['id'], $updated_conversation_steps)) { log_to_file("process_upcoming_conversations - Updated conversation_steps!"); } } catch (Exception $e) { error_log("Failed to generate conversation ID {$conversation['id']}: " . $e->getMessage()); } } action_log("process_upcoming_conversations - Finished processing {$conversations_count} conversations"); return $conversations_count; } else { return 0; } } private static function update_messages_with_merged_output($conversation_id, $merged_output) { // log_to_file("update_messages_with_merged_output - merged_output: ", $merged_output); global $wpdb; $message_table = $wpdb->prefix . 'rl_mailwarmer_messages'; $merged_output = json_decode($merged_output); $merged_output_length = count($merged_output); // log_to_file("update_messages_with_merged_output - merged_output length: ", $merged_output_length); // Fetch all messages for the given conversation ID $messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $message_table WHERE conversation_id = %d ORDER BY `scheduled_for_timestamp` ASC", $conversation_id ), ARRAY_A ); $messages_length = count($messages); // log_to_file("update_messages_with_merged_output - Found {$messages_length} messages: ", $messages); if (empty($messages)) { throw new Exception("No messages found for conversation ID $conversation_id."); } // Iterate through messages and merged output foreach ($messages as $index => $message) { if (!isset($merged_output[$index])) { // log_to_file("update_messages_with_merged_output - No matching message & output found. Skipping! {$index} "); continue; // Skip if there's no corresponding merged output } else { // log_to_file("update_messages_with_merged_output - Processing message {$index} "); } // $merged = json_decode($merged_output[$index], true); $merged = $merged_output[$index]; // log_to_file("update_messages_with_merged_output - Message Data:", $merged); // $merged_length = count($merged); // log_to_file("update_messages_with_merged_output - Merged {$merged_length}:", $merged); // log_to_file("update_messages_with_merged_output - merged[status]:", $merged->status); // Prepare updated data // $updated_data = []; $updated_data = [ 'status' => $merged->status, 'scheduled_for_timestamp' => $merged->scheduled_for, 'from_email' => $merged->from, 'to_email' => json_encode($merged->to), 'cc' => json_encode($merged->cc), 'subject' => $merged->subject, 'body' => $merged->body, ]; // log_to_file("update_messages_with_merged_output - Updated Data: ", $updated_data); // Update the database $db_update_message_result = $wpdb->update( $message_table, $updated_data, ['id' => intval($message['id'])], // Ensure the correct message ID is passed here ['%s', '%s', '%s', '%s', '%s', '%s', '%s'], // Format specifiers ['%d'] ); if ($db_update_message_result) { log_to_file("update_messages_with_merged_output - DB Update result: ", $db_update_message_result); } else { $last_query = $wpdb->last_query; log_to_file("update_messages_with_merged_output - Problem updating DB: ", $last_query); return false; } } return true; } public static function merge_timeline_with_ai_response($conversation_string, $ai_response) { $conversation = json_decode($conversation_string, true); $conversation_type = gettype($conversation); $ai_response_type = gettype($ai_response); // log_to_file("merge_timeline_with_ai_response - Conversation type: ", $conversation_type); // log_to_file("merge_timeline_with_ai_response - AI Response type: ", $ai_response_type); // log_to_file("merge_timeline_with_ai_response - Conversation: ", $conversation); // log_to_file("merge_timeline_with_ai_response - AI Response: ", $ai_response); // Ensure both arrays have the same number of elements // log_to_file("merge_timeline_with_ai_response - Counting arrays"); $merged = []; $count_timeline = count($conversation); $count_ai_response = count($ai_response); // log_to_file("merge_timeline_with_ai_response - Timeline Count: {$count_timeline} AI Response Count: {$count_ai_response}"); if ($count_ai_response >= $count_timeline) { // Trim the AI responses to match the timeline count // log_to_file("merge_timeline_with_ai_response - Trimming AI messages to merge with conversation"); $ai_response = array_slice($ai_response, 0, $count_timeline); } elseif ($count_ai_response < $count_timeline) { // log_to_file("merge_timeline_with_ai_response - AI response is too short to merge with conversation"); throw new Exception('The number of AI responses is less than the timeline steps.'); } // log_to_file("merge_timeline_with_ai_response - Attempting merge"); // Merge corresponding elements foreach ($conversation as $index => $timeline_step) { $ai_message = $ai_response[$index]; $timeline_step_type = gettype($timeline_step); $ai_message_type = gettype($ai_message); // log_to_file("merge_timeline_with_ai_response - Conversation step type: ", $timeline_step_type); // log_to_file("merge_timeline_with_ai_response - AI Message type: ", $ai_message_type); $merge_result = array_merge($timeline_step, $ai_message); // log_to_file("merge_timeline_with_ai_response - Merged: ", $merge_result); $merged[] = $merge_result; } // log_to_file("merge_timeline_with_ai_response - Merged Steps: ", $merged); // Return the merged result as JSON return $merged; } /** * Update the conversation steps for a given conversation ID. * * @param int $conversation_id The ID of the conversation to update. * @param string $ai_response The AI-generated response in JSON format. * @return bool|WP_Error True on success, WP_Error on failure. */ public static function update_conversation_steps($conversation_id, $ai_response) { global $wpdb; // log_to_file("update_conversation_steps - Running! $conversation_id ", $ai_response); // Validate input if (empty($conversation_id) || !is_int($conversation_id)) { return new WP_Error('invalid_conversation_id', __('Invalid conversation ID.', 'rl-mailwarmer')); } if (empty($ai_response) || !is_string($ai_response)) { return new WP_Error('invalid_ai_response', __('Invalid AI response.', 'rl-mailwarmer')); } // Define the table name $table_name = $wpdb->prefix . 'rl_mailwarmer_conversation'; // Prepare the data $data = [ 'conversation_steps' => $ai_response, // Save the JSON response as a string ]; $where = [ 'id' => $conversation_id, ]; // Update the database $result = $wpdb->update($table_name, $data, $where, ['%s'], ['%d']); if ($result === false) { return new WP_Error('db_update_failed', __('Failed to update the conversation steps.', 'rl-mailwarmer')); } // log_to_file("update_conversation_steps - Updated conversation steps for #$conversation_id"); return true; } /** * Generate a conversation blueprint for a campaign. * * @param int $campaign_id The ID of the campaign. * @param int $number_responses The number of responses/messages to schedule. Defaults to 0. * @return int The ID of the created conversation in the database. * @throws Exception If required data is missing or saving fails. */ public static function generate_conversation_blueprint($campaign_id, $num_particpants = 1, $number_responses = 1, $start_date = false, $end_date = false, $filled_dates = []) { global $wpdb; // log_to_file("generate_conversation_blueprint - {$campaign_id} {$number_responses} {$start_date} {$end_date}", $filled_dates); $campaign_limited = get_post_meta($campaign_id, 'campaign_limited', true); // Fetch the start date for the campaign if one isn't passed if (!$start_date) { $start_date = get_post_meta($campaign_id, 'start_date', true); if (!$start_date) { throw new Exception(__('generate_conversation_blueprint - Campaign start date is missing.', 'rl-mailwarmer')); } } // log_to_file("generate_conversation_blueprint - Generating conversation with starting date: $start_date & end date: $end_date"); // Step 1: Generate placeholders for the conversation steps $array_args = ['step' => '', 'status' => 'scheduled']; $conversation_steps_placeholder = array_fill(0, $number_responses, $array_args); // log_to_file("generate_conversation_blueprint - Conversation Steps Placeholder", $conversation_steps_placeholder); $conversation_steps = self::add_timestamps_to_conversation($conversation_steps_placeholder, $start_date, $end_date, $filled_dates); // log_to_file("generate_conversation_blueprint - Conversation Steps", $conversation_steps); // AI Prompt Defaults $args = [ 'initiated_by' => null, 'received_by' => null, 'subject' => null, 'length' => null, 'num_participants' => $num_particpants, 'num_responses' => $number_responses, 'reply_pool' => [], 'cc_pool' => [], ]; // $args = wp_parse_args($args, $defaults); // $args['num_responses'] = $number_responses; $campaign_tracking_id = get_post_meta($campaign_id, 'campaign_tracking_id', true); // Fetch campaign target profession // log_to_file("generate_conversation_blueprint - Target Profession"); $target_profession = get_field('target_profession', $campaign_id); $from_emails = get_post_meta($campaign_id, 'email_accounts', true); log_to_file("generate_conversation_blueprint - From Emails", $from_emails); if (!$from_emails) { throw new Exception(__('generate_conversation_blueprint - from_email is missing.', 'rl-mailwarmer')); } $from_emails_key = array_rand($from_emails, 1); $from_address = get_the_title($from_emails[$from_emails_key]); // log_to_file("generate_conversation_blueprint - From Email ID", $from_emails[$from_emails_key]); // log_to_file("generate_conversation_blueprint - From Email Address", $from_address); // $from_pool = []; // foreach ($from_emails as $email_id) { // } // $email = get_post($from_email); // log_to_file("generate_conversation_blueprint - From pool: ", $from_pool); // Fetch scrubber pool if 'received_by' is not passed if (!isset($args['received_by'])) { $scrubber_pool_full = self::get_email_accounts_for_pool($campaign_id, 'scrubber'); // Pick $num_particpants random addresses $num_scrubbers = min($num_particpants, count($scrubber_pool_full)); $scrubber_pool = array_rand(array_flip($scrubber_pool_full), $num_scrubbers); // Ensure $random_keys is an array if ($num_scrubbers === 1) { $args['received_by'] = [$scrubber_pool]; } else { $args['received_by'] = $scrubber_pool; } log_to_file("generate_conversation_blueprint - Scrubber Pool: ", $args['received_by']); } // Fetch the CC Pool // log_to_file("generate_conversation_blueprint - CC Pool"); // $cc_pool = self::get_email_accounts_for_pool($campaign_id, 'cc'); // // Get up to 4 random items // $num_to_select = min(4, count($cc_pool)); // Ensure we don't request more items than are available // $random_keys = array_rand($cc_pool, $num_to_select); // If only one item is selected, ensure it's returned as an array // $args['cc_pool'] = is_array($random_keys) ? array_intersect_key($cc_pool, array_flip($random_keys)) : [$cc_pool[$random_keys]]; // log_to_file("generate_conversation_blueprint - Reply Pool"); // Fetch the Reply pool if there will be any replies needed if ( intval($args['num_responses']) > 0) { $reply_pool = self::get_email_accounts_for_pool($campaign_id, 'reply'); $args['reply_pool'] = array_values(array_intersect($args['received_by'], $reply_pool)); log_to_file("generate_conversation_blueprint - Reply Pool: ", $args['reply_pool']); } // log_to_file("generate_conversation_blueprint - Conversation Topics"); // Fetch topics for the conversation if 'subject' is not passed if (!$args['subject']) { $default_topics = get_option('options_default_topic_pool') ? rl_get_textarea_meta_as_array("option", "default_topic_pool") : []; $campaign_topics = get_post_meta($campaign_id, 'campaign_conversation_topics', true) ? rl_get_textarea_meta_as_array($campaign_id, 'campaign_conversation_topics') : []; // $topics_2 = get_post_meta($campaign_id, 'campaign_conversation_topics', true); // log_to_file("generate_conversation_blueprint - Default topics:", $default_topics); // log_to_file("generate_conversation_blueprint - Campaign topics:", $campaign_topics); // log_to_file("generate_conversation_blueprint - Campaign topics2 : $topics_2"); $all_topics = array_merge($default_topics, $campaign_topics); // $all_topics_count = count($all_topics); // log_to_file("generate_conversation - All topics $all_topics_count: ", $all_topics); if (!empty($all_topics)) { $args['subject'] = $all_topics[array_rand($all_topics)]; } else { $args['subject'] = __('General Inquiry', 'rl-mailwarmer'); } } log_to_file("generate_conversation_blueprint - Conversation args", $args); // Generate the prompt if ($campaign_limited || (intval($args['num_responses']) === 1) ) { $chat_model = 'gpt-4o-mini'; // $chat_model = 'o3-mini'; $prompt = sprintf( "Generate a single JSON email from {$from_address} to one or more addresses in 'to_pool' with no replies. Fields to return: from, to, cc, subject, body, valediction. The valediction should be separate from the body. Return only JSON, no notes\n%s", json_encode([ 'profession' => $target_profession, 'from_address' => $from_address, 'to_pool' => $args['received_by'], 'subject' => $args['subject'], 'num_of_replies' => 1, 'available_to_cc' => $args['cc_pool'], ]) ); } else { $chat_model = 'gpt-4o'; // $chat_model = 'o3-mini'; $num_replies = intval($args['num_responses']) + 1; $prompt = sprintf( "Generate a JSON email conversation with distinct participant personalities; up to 5%% errors; initiating email can be sent to multiple people; replies and follow-ups only from the sender or addresses in both 'can_reply' AND 'to_pool'. Fields to return: from, to, cc, subject, body, valediction. The valediction should be separate from the body. Return only JSON, no notes\n%s", json_encode([ 'profession' => $target_profession, 'from_address' => $from_address, 'to_pool' => $args['received_by'], 'subject' => $args['subject'], 'num_of_replies' => $num_replies, 'can_reply' => $args['reply_pool'], 'available_to_cc' => $args['cc_pool'], ]) ); } // log_to_file("From prompt: ", $prompt); // Step 2: Save the conversation to the database $conversation_data = [ 'campaign_ID' => $campaign_id, 'email_account_id' => $from_emails[$from_emails_key], 'created_at' => current_time('mysql'), 'status' => 'new', 'first_message_timestamp' => $conversation_steps[0]['scheduled_for'], 'prompt' => $prompt, 'model' => $chat_model, 'conversation_steps' => json_encode($conversation_steps), 'campaign_tracking_id' => $campaign_tracking_id, ]; // log_to_file("generate_conversation_blueprint - Conversation Data: ", $conversation_data); $conversation_id = RL_MailWarmer_DB_Helper::insert_conversation($conversation_data); // $conversation_id = 69; // $conversation_table = $wpdb->prefix . 'rl_mailwarmer_conversation'; // $wpdb->insert($conversation_table, $conversation_data); // $conversation_id = $wpdb->insert_id; if (!$conversation_id) { throw new Exception(__('generate_conversation_blueprint - Failed to save the conversation blueprint.', 'rl-mailwarmer')); } // Step 3: Save the individual message placeholders to the messages table // $message_table = $wpdb->prefix . 'rl_mailwarmer_messages'; $previous_message_id = null; // Initialize the previous message ID foreach ($conversation_steps as $index => $step) { $is_first_message = $index === 0 ? 1 : 0; $message_data = [ 'campaign_ID' => $campaign_id, 'campaign_tracking_id' => $campaign_tracking_id . '-' .$conversation_id, 'email_account_id' => $from_emails[$from_emails_key], 'conversation_ID' => $conversation_id, 'scheduled_for_timestamp' => $step['scheduled_for'], 'status' => 'new', 'first_message' => $is_first_message, 'previous_message_id' => $previous_message_id, // Include previous message ID here ]; log_to_file("generate_conversation_blueprint - Message Data: ", $message_data); $message_id = RL_MailWarmer_DB_Helper::insert_message($message_data); if (!$message_id) { throw new Exception(__('generate_conversation_blueprint - Failed to save the message blueprint.', 'rl-mailwarmer')); } // Save the message_id back to the current step $conversation_steps[$index]['message_id'] = $message_id; // Update the previous_message_id for the next iteration $previous_message_id = $message_id; } // Update the conversation with the message IDS $update_data['conversation_steps'] = json_encode($conversation_steps); // log_to_file("generate_conversation_blueprint - Update Data: ", $update_data); RL_MailWarmer_DB_Helper::update_conversation($conversation_id, $update_data); return $conversation_steps; } /** * Generate a single conversation for a campaign - On the admin page sidebar * * @param int $campaign_id The ID of the campaign. * @param array $args Optional arguments to customize the conversation. * @return int|WP_Error The ID of the created conversation or WP_Error on failure. */ public static function generate_conversation($campaign_id, $args = []) { if (empty($campaign_id)) { return new WP_Error('invalid_campaign', __('Campaign ID is required.', 'rl-mailwarmer')); } // Fetch campaign details $campaign = get_post($campaign_id); if (!$campaign || $campaign->post_type !== 'campaign') { return new WP_Error('invalid_campaign', __('Invalid campaign.', 'rl-mailwarmer')); } // Defaults $defaults = [ 'initiated_by' => null, 'received_by' => null, 'subject' => null, 'length' => null, 'num_participants' => null, 'num_responses' => null, 'reply_pool' => [], 'cc_pool' => [], ]; $args = wp_parse_args($args, $defaults); // Fetch email accounts for this campaign's domain if 'initiated_by' is not passed if (!$args['initiated_by']) { $campaign_domain = get_field('domain', $campaign_id); $email_accounts = get_posts([ 'post_type' => 'email-account', 'meta_query' => [ [ 'key' => 'domain', 'value' => $campaign_domain->ID, 'compare' => '=', ], ], 'posts_per_page' => -1, 'fields' => 'ids', ]); if (empty($email_accounts)) { return new WP_Error('no_initiator', __('No email accounts available for the campaign domain.', 'rl-mailwarmer')); } $args['initiated_by'] = $email_accounts[array_rand($email_accounts)]; } if ( (int) $args['length'] > 2 ) { $chat_model = 'gpt-4o'; } else { $chat_model = 'gpt-4o-mini'; } // fetch the email address // $from_id = $args['initiated_by']; // $from_email = get_the_title($from_id); // $args['initiated_by'] = $from_email; // Set length defaults for participants and responses if ($args['length']) { $length_defaults = [ 'extra-long' => ['num_participants' => mt_rand(2, 8), 'num_responses' => mt_rand(5, 8)], 'long' => ['num_participants' => mt_rand(3, 6), 'num_responses' => mt_rand(4, 6)], 'medium' => ['num_participants' => mt_rand(3, 5), 'num_responses' => mt_rand(3, 5)], 'short' => ['num_participants' => mt_rand(2, 4), 'num_responses' => mt_rand(1, 3)], 'single' => ['num_participants' => mt_rand(2, 6), 'num_responses' => 0], ]; if (isset($length_defaults[$args['length']])) { $defaults = $length_defaults[$args['length']]; // log_to_file("Length defaults: ", $defaults['num_participants']); if (isset($args['num_participants'])) { unset($defaults['num_participants']); } if (isset($args['num_responses'])) { unset($defaults['num_responses']); } $args = array_merge($args, $length_defaults[$args['length']]); } } else { if (!$args['num_participants']) { $args['num_participants'] = 2; } if (!$args['num_responses']) { $args['num_responses'] = 0; } } // Fetch topics for the conversation if 'subject' is not passed if (!$args['subject']) { $default_topics = get_option('options_default_topic_pool') ? rl_get_textarea_meta_as_array("option", "default_topic_pool") : []; $campaign_topics = get_post_meta($campaign_id, 'campaign_conversation_topics', true) ? rl_get_textarea_meta_as_array($campaign_id, 'campaign_conversation_topics') : []; $all_topics = array_merge($default_topics, $campaign_topics); // $all_topics_count = count($all_topics); // log_to_file("generate_conversation - All topics $all_topics_count: ", $all_topics); if (!empty($all_topics)) { $args['subject'] = $all_topics[array_rand($all_topics)]; } else { $args['subject'] = __('General Inquiry', 'rl-mailwarmer'); } } // Fetch campaign duration $start_date = get_field('start_date', $campaign_id); $warmup_period = get_field('warmup_period', $campaign_id); $end_date = date('Ymd', strtotime("+{$warmup_period} weeks", strtotime($start_date))); // Fetch scrubber pool if 'received_by' is not passed if (!$args['received_by']) { $args['received_by'] = self::get_email_accounts_for_pool($campaign_id, 'scrubber_test'); // $args['received_by'] = get_field('scrubber_pool', 'option') ? rl_get_textarea_meta_as_array('option', 'options_scrubber_pool') : ''; } // Fetch the CC Pool $args['cc_pool'] = self::get_email_accounts_for_pool($campaign_id, 'cc'); // Fetch the Reply pool if there will be any replies needed if ( intval($args['num_responses']) > 0) { $args['reply_pool'] = self::get_email_accounts_for_pool($campaign_id, 'reply'); } // Fetch campaign target profession $target_profession = get_field('target_profession', $campaign_id); // Generate the prompt $prompt = sprintf( "Generate a JSON email conversation with distinct participant personalities, up to 5%% errors, and replies only from the sender or addresses in both 'can_reply' and 'to_pool'. Include: from, to, cc, subject, body\n%s", json_encode([ 'profession' => $target_profession, 'from' => $args['initiated_by'], 'to_pool' => $args['received_by'], 'subject' => $args['subject'], 'num_of_replies' => $args['num_responses'], 'num_of_participants' => $args['num_participants'], 'can_reply' => $args['reply_pool'], 'available_to_cc' => $args['cc_pool'], ], JSON_PRETTY_PRINT) ); log_to_file("generate_conversation - Prompt: $prompt"); // Save the conversation to the database $conversation_steps = []; // Placeholder until AI generates steps $conversation_id = RL_MailWarmer_DB_Helper::insert_conversation($campaign_id, $conversation_steps, $prompt, $chat_model); if (!$conversation_id) { return new WP_Error('db_error', __('Failed to create conversation.', 'rl-mailwarmer')); } // Generate AI content for the conversation $ai_response[] = self::clean_ai_response(self::generate_ai_conversation($prompt,$chat_model)); // $ai_response = self::clean_ai_response(get_post_meta($campaign_id, 'last_ai_response', true)); if (is_wp_error($ai_response[0])) { return $ai_response[0]; // Handle AI generation failure gracefully } // Update the conversation with generated steps - ?????????????????? // self::update_conversation_steps($conversation_id, $ai_response); log_to_file("generate_conversation - AI response: ", $ai_response); // update_post_meta($campaign_id, 'last_ai_response', wp_slash($ai_response)); // Parse the AI response and save messages // $parsed_steps = self::parse_ai_response($ai_response); $array_args = ['step' => '']; $parsed_steps = array_fill(0, $args['num_responses'], $array_args ); // Save each timestamp into the parsed steps $parsed_steps = self::add_timestamps_to_conversation($parsed_steps, $start_date); log_to_file("generate_conversation - parsed_steps: ", $parsed_steps); foreach ($parsed_steps as $index => $step) { if (!empty($step['scheduled_for'])) { $is_first_message = ($index === 0); if (is_array($step['to'])) { $step['to'] = implode(', ', $step['to']); } if (is_array($step['cc'])) { $step['cc'] = implode(', ', $step['cc']); } // log_to_file("generate_conversation - message body: ", $step['body']); // RL_MailWarmer_DB_Helper::insert_message( // $campaign_id, // $conversation_id, // $step['scheduled_for'], // $step['from'], // $step['to'], // $step['cc'], // $step['subject'], // $step['body'], // $is_first_message // ); } } return $conversation_id; } /** * Fetch all published email accounts matching the campaign domain with "include_in_cc_pool" set to true. * * @param int $campaign_id The ID of the campaign. * @param string $pool The name of the pool. Valid options are: cc, reply * @return array The email addresses to include in the CC pool. */ private static function get_email_accounts_for_pool($campaign_id, $pool) { // Get the domain associated with the campaign $domain_id = get_post_meta($campaign_id, 'domain', true); if (empty($domain_id)) { return []; // Return an empty array if no domain is found } // Query email accounts $args = [ 'post_type' => 'email-account', 'post_status' => 'publish', 'meta_query' => [ 'relation' => 'AND', [ 'key' => 'include_in_' . $pool . '_pool', 'value' => true, 'compare' => '=', ], // [ // 'key' => 'domain', // 'value' => $domain_id, // 'compare' => '=', // ], ], 'fields' => 'ids', // Retrieve only IDs for efficiency 'posts_per_page' => -1, // Fetch all matching posts ]; $query = new WP_Query($args); $email_pool = []; if ($query->have_posts()) { foreach ($query->posts as $post_id) { $email_address = get_the_title($post_id); if (!empty($email_address)) { $email_pool[] = $email_address; } } } return $email_pool; } /** * Add timestamps to parsed conversation steps. * * @param array $parsed_steps The parsed steps of the conversation. * @param string $start_date The starting date for the timestamps. * @return array The updated conversation steps with timestamps and step indices. */ private static function add_timestamps_to_conversation($parsed_steps, $start_date, $end_date = NULL, $filled_dates = []) { // Ensure parsed_steps is an array if (!is_array($parsed_steps) || empty($parsed_steps)) { throw new Exception(__('Parsed steps must be a non-empty array.', 'rl-mailwarmer')); } // Initialize the updated steps array $updated_steps = []; // Iterate through each step and assign timestamps foreach ($parsed_steps as $index => $step) { // If this is not the first step, ensure the start_date is at least 5 minutes after the previous step if ($index > 0) { $previous_timestamp = strtotime($updated_steps[$index - 1]['scheduled_for']); $start_date = date('Y-m-d H:i:s', max($timestamp, $previous_timestamp + 300)); // 300 seconds = 5 minutes } // Use the helper function to generate a random timestamp $timestamp = self::generate_random_date_time($start_date, $end_date, $filled_dates ); // log_to_file("add_timestamps_to_conversation - Returned random timestamp: $timestamp"); // Update the step with the scheduled timestamp and step index $step['scheduled_for'] = date('Y-m-d H:i:s', $timestamp); $step['step'] = $index + 1; // Update the starting date for the next iteration $start_date = date('Y-m-d H:i:s', $timestamp); // Append the updated step $updated_steps[] = $step; } // log_to_file("add_timestamps_to_conversation - Updated steps: ", $updated_steps); return $updated_steps; } /** * Generate a semi-randomized timestamp with a random date and time. * * @param string $start_date The start date in 'Y-m-d' format. Defaults to today. * @param string $end_date The end date in 'Y-m-d' format. Defaults to today + 7 days. * @param string $start_time The start time in 'H:i' format (e.g., '08:00'). Defaults to '08:00'. * @param string $end_time The end time in 'H:i' format (e.g., '18:00'). Defaults to '18:00'. * @return int The random timestamp. */ private static function generate_random_date_time($start_date = null, $end_date = null, $filled_dates = [], $start_time = '07:20', $end_time = '21:00' ) { // Default dates $start_date = $start_date ?: date('Y-m-d H:i'); $end_date = $end_date ?: date('Y-m-d H:i', strtotime($start_date . ' +8 days')); log_to_file("generate_random_date_time - Generating random date time between $start_date & $end_date"); $start_date_timestamp = strtotime($start_date); $end_date_timestamp = strtotime($end_date); $retry = false; if ($end_date_timestamp < $start_date_timestamp) { throw new Exception('generate_random_date_time - End date must be greater than or equal to start date.'); } else if ( $end_date_timestamp === $start_date_timestamp ) { // Single-part conversations have the same start & end date $random_date = date('Y-m-d', $start_date_timestamp); // log_to_file("generate_random_date_time - Using the same start & end date"); } else { // Generate a random date within the timeframe log_to_file("generate_random_date_time - Finding a date that's not in \$filled_dates"); do { if ($retry) { // log_to_file("generate_random_date_time - Regenerating random date because it was found in filled_dates."); } // Generate a random date between the start and end dates $random_date_timestamp = mt_rand($start_date_timestamp, $end_date_timestamp); $random_date = date('Y-m-d', $random_date_timestamp); $retry = true; log_to_file("generate_random_date_time - Random date $random_date"); log_to_file("generate_random_date_time - Filled Dates: ", $filled_dates); } while (in_array($random_date, $filled_dates)); log_to_file("generate_random_date_time - Random date found!"); } // Generate a random time within the specified range for the selected date log_to_file("generate_random_date_time - calling generate_random_time( $random_date, $start_time, $end_time)"); $random_time_timestamp = self::generate_random_time($random_date, $start_time, $end_time); // log_to_file("generate_random_date_time - Returned random timestamp: $random_time_timestamp"); return $random_time_timestamp; } /** * Generate a random timestamp for a given date between specific hours. * * @param string $date The date in 'Y-m-d' format. * @param string $start_time The start time in 'H:i' format. * @param string $end_time The end time in 'H:i' format. * @return int The random timestamp. */ private static function generate_random_time($date, $start_time, $end_time) { // Convert start and end times to timestamps $start_timestamp = strtotime("$date $start_time"); $end_timestamp = strtotime("$date $end_time"); // log_to_file("generate_random_time - Start: $start_timestamp End: $end_timestamp"); if ($end_timestamp < $start_timestamp) { throw new Exception('generate_random_time - End time must be greater or equal to start time.'); } // Generate a random timestamp within the time range $timestamp = mt_rand($start_timestamp, $end_timestamp); // log_to_file("generate_random_time - Random Timestamp: $timestamp"); return $timestamp; } /** * Generate AI content for the conversation using OpenAI's ChatGPT API. * * @param string $prompt The prompt to send to ChatGPT. * @return string|WP_Error The AI response or WP_Error on failure. */ public static function generate_ai_conversation($prompt, $model) { // $api_key = getenv('OPENAI_API_KEY'); $api_key = get_field('chatgpt_api_key', 'option'); if (empty($api_key)) { return new WP_Error('missing_api_key', __('OpenAI API key is not configured.', 'rl-mailwarmer')); } try { $client = \OpenAI::client($api_key); $response = $client->chat()->create([ 'model' => $model, 'messages' => [ ['role' => 'system', 'content' => 'You are a helpful assistant specialized in generating email conversations.'], ['role' => 'user', 'content' => $prompt], ], 'temperature' => 0.7, 'max_tokens' => 3000, ]); return self::clean_ai_response($response['choices'][0]['message']['content']); } catch (\Exception $e) { return new WP_Error('api_error', $e->getMessage()); } } /** * Clean the AI response by removing Markdown-style code block delimiters. * * @param string $response The raw AI response. * @return string The cleaned JSON string. */ private static function clean_ai_response($response) { // Use regex to remove the ```json and ``` delimiters return preg_replace('/^```json\s*|```$/m', '', trim($response)); } private static function normalize_conversation_format($ai_response_string) { log_to_file("normalize_conversation_format - Running"); // log_to_file("normalize_conversation_format - Response: {}"); // Decode JSON string to PHP array/object $decoded = json_decode($ai_response_string, true); // If decoding failed, return empty array if (!$decoded) { log_to_file("normalize_conversation_format - Failed to json_decode ai_response_string"); return []; } // Check if the response is a flat object (single email) if (isset($decoded['from'])) { // It's a single email, wrap it in an array log_to_file("normalize_conversation_format - Single response. Converting to multi-dimensional array"); return [$decoded]; } // Already a multi-dimensional array, return as is log_to_file("normalize_conversation_format - Returning as is"); return $decoded; } } add_action('admin_menu', function () { add_menu_page( __('Conversations', 'rl-mailwarmer'), // Page title __('Conversations', 'rl-mailwarmer'), // Menu title 'manage_options', // Capability 'rl-mailwarmer-conversations', // Menu slug 'rl_mailwarmer_render_conversations_page', // Callback function 'dashicons-email', // Icon 8 // Position ); }); /** * Render the Conversations admin page with checkboxes and a Delete button. */ function rl_mailwarmer_render_conversations_page() { if (!current_user_can('manage_options')) { return; } global $wpdb; $table_name = $wpdb->prefix . 'rl_mailwarmer_conversations'; // Conversation table $conversations = $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC"); ?>

id); ?> campaign_id); ?> created_at); ?> conversation_steps); ?>
prompt); ?>

get_var("SELECT COUNT(*) FROM {$wpdb->prefix}rl_mailwarmer_conversation"); $conversations = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}rl_mailwarmer_conversation ORDER BY created_at DESC LIMIT %d OFFSET %d", $per_page, $offset )); return ['conversations' => $conversations, 'total' => $total]; } add_action('wp_ajax_rl_delete_conversations', function () { global $wpdb; if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied.', 'rl-mailwarmer')]); } $conversation_ids = isset($_POST['conversation_ids']) ? array_map('intval', $_POST['conversation_ids']) : []; if (empty($conversation_ids)) { wp_send_json_error(['message' => __('No conversations selected.', 'rl-mailwarmer')]); } $table_name = $wpdb->prefix . 'rl_mailwarmer_conversation'; // Delete selected conversations $placeholders = implode(',', array_fill(0, count($conversation_ids), '%d')); $query = "DELETE FROM $table_name WHERE id IN ($placeholders)"; $result = $wpdb->query($wpdb->prepare($query, $conversation_ids)); if ($result === false) { wp_send_json_error(['message' => __('Failed to delete conversations.', 'rl-mailwarmer')]); } wp_send_json_success(['message' => __('Conversations deleted successfully.', 'rl-mailwarmer')]); }); // Add the metabox add_action('add_meta_boxes', 'rl_mailwarmer_add_process_conversations_metabox'); function rl_mailwarmer_add_process_conversations_metabox() { add_meta_box( 'rl-process-conversations', __('Process Upcoming Conversations', 'rl-mailwarmer'), 'rl_mailwarmer_process_conversations_metabox_callback', 'campaign', 'side', 'default' ); } // Metabox callback function function rl_mailwarmer_process_conversations_metabox_callback($post) { // Nonce field for security wp_nonce_field('rl_process_conversations_nonce', 'rl_process_conversations_nonce_field'); ?>
admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('rl_process_conversations_nonce'), ]); } } // AJAX handler add_action('wp_ajax_rl_process_upcoming_conversations', 'rl_mailwarmer_process_upcoming_conversations_handler'); function rl_mailwarmer_process_upcoming_conversations_handler() { check_ajax_referer('rl_process_conversations_nonce', 'security'); $post_id = intval($_POST['post_id']); if (!$post_id) { wp_send_json_error(__('Invalid campaign ID.', 'rl-mailwarmer')); } try { // Call the function to process upcoming conversations RL_MailWarmer_Conversation_Handler::process_upcoming_conversations($post_id); wp_send_json_success(__('Conversations processed successfully.', 'rl-mailwarmer')); } catch (Exception $e) { wp_send_json_error(__('Error processing conversations: ', 'rl-mailwarmer') . $e->getMessage()); } } /** * Add a metabox to the WP dashboard for monitoring and processing the conversation queue. */ add_action('wp_dashboard_setup', function () { wp_add_dashboard_widget( 'rl_mailwarmer_conversation_queue', __('Conversation Queue', 'rl-mailwarmer'), 'rl_mailwarmer_render_conversation_queue_widget' ); }); /** * Render the Message Queue dashboard widget. */ function rl_mailwarmer_render_conversation_queue_widget() { global $wpdb; // Count past-due conversations $table_name = $wpdb->prefix . 'rl_mailwarmer_conversations'; $start_time = date('Y-m-d H:i:s', strtotime('-96 hours')); $end_time = date('Y-m-d H:i:s', strtotime('+12 hours')); $end_of_today = date('Y-m-d 11:59:59', current_time('timestamp')); $past_due_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE status = %s AND first_message_timestamp BETWEEN %s AND %s", 'new', $start_time, $end_time ) ); // $conversations = $wpdb->get_results( // $wpdb->prepare( // "SELECT * FROM $conversation_table WHERE status = %s AND first_message_timestamp BETWEEN %s AND %s ORDER BY first_message_timestamp ASC LIMIT 5", // 'new', // $start_time, // $end_time // ), // ARRAY_A // ); ?>

__('Invalid nonce.', 'rl-mailwarmer')]); } // Ensure the user has permission if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied.', 'rl-mailwarmer')]); } try { // Process pending conversations // log_to_file("wp_ajax_rl_mailwarmer_process_conversation_queue - Trying process_pending_conversations()"); $processed_count = RL_MailWarmer_Conversation_Handler::process_upcoming_conversations(); wp_send_json_success(['processed_count' => $processed_count]); } catch (Exception $e) { wp_send_json_error(['message' => $e->getMessage()]); } });