register_index_hooks(); add_action('wp_ajax_aieo_chat_generate_embeddings', array($this, 'ajax_generate_embeddings')); add_action('wp_ajax_aieo_chat_generate_taxonomy_embeddings', array($this, 'ajax_generate_taxonomy_embeddings')); add_action('wp_ajax_aieo_chat_regenerate_embedding', array($this, 'ajax_regenerate_embedding')); add_action('wp_ajax_aieo_chat_delete_embeddings', array($this, 'ajax_delete_embeddings')); add_action('wp_ajax_aieo_chat_get_engine_stats', array($this, 'ajax_get_engine_stats')); add_action('wp_ajax_aieo_chat_update_embedding_model', array($this, 'ajax_update_embedding_model')); add_action('wp_ajax_aieo_chat_get_delete_counts', array($this, 'ajax_get_delete_counts')); add_action('wp_ajax_aieo_chat_get_available_engines', array($this, 'ajax_get_available_engines')); add_action('wp_ajax_aieo_chat_get_delete_stats', array($this, 'ajax_get_delete_stats')); add_action('wp_ajax_aieo_chat_save_last_trained_engine', array($this, 'ajax_save_last_trained_engine')); } public function generate_embedding($post_id, $post_type = 'product', $force = false) { if (!AIEO_Chat_Indexing::enabled()) { return new WP_Error('aieo_chat_indexing_off', AIEO_Chat_Indexing::refusal()); } $content = AI_eShop_Chat_Content_Extractor::extract_content($post_id, $post_type); if (empty($content)) { return new WP_Error('no_content', __('No content to generate embedding', 'ai-eshop-optimizer')); } $content_hash = md5($content); if (!$force) { $db = AI_eShop_Chat_Database_Manager::get_instance(); $existing = $db->get_embedding($post_id); if ($existing && $existing['content_hash'] === $content_hash) { return true; } } wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_openai_embedding_model', 'options'); wp_cache_delete('aieo_chat_embedding_model', 'options'); $embedding_provider = get_option('aieo_embedding_provider', 'voyage'); if ($embedding_provider === 'openai') { $model = get_option('aieo_chat_openai_embedding_model', 'text-embedding-3-large'); $api = AI_eShop_Chat_OpenAI_API::get_instance(); $embedding = $api->generate_embedding($content, $model); } else { $model = get_option('aieo_chat_embedding_model', 'voyage-2'); $api = AI_eShop_Chat_Anthropic_API::get_instance(); $embedding = $api->generate_embedding($content, $model, AIEO_AI_Sources::EMB_INDEX); } if (is_wp_error($embedding)) { return $embedding; } $db = AI_eShop_Chat_Database_Manager::get_instance(); $result = $db->store_embedding($post_id, $embedding, $content_hash, $post_type, $model); if (!$result) { return new WP_Error('storage_failed', __('Failed to store embedding', 'ai-eshop-optimizer')); } return true; } public function generate_embeddings_batch($post_ids, $post_type = 'product', $deduplicated = 0, $engine_type = null) { $results = array(); if (!AIEO_Chat_Indexing::enabled()) { return $results; } $post_ids = array_values(array_unique(array_map('intval', $post_ids))); if (get_option('aieo_chat_prioritize_by_sales', '0') === '1' && $post_type === 'product') { $post_ids = $this->sort_by_sales($post_ids); } elseif (get_option('aieo_chat_prioritize_top_sellers', '0') === '1' && $post_type === 'product') { $post_ids = $this->sort_by_sales($post_ids); } if ($engine_type !== null) { if (strpos($engine_type, 'text-embedding') === 0) { $embedding_provider = 'openai'; $model = $engine_type; } else { $embedding_provider = 'voyage'; $model = $engine_type; } } else { wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_openai_embedding_model', 'options'); wp_cache_delete('aieo_chat_embedding_model', 'options'); $embedding_provider = get_option('aieo_embedding_provider', 'voyage'); if ($embedding_provider === 'openai') { $model = get_option('aieo_chat_openai_embedding_model', 'text-embedding-3-large'); } else { $model = get_option('aieo_chat_embedding_model', 'voyage-2'); } } $chunk_size = 20; $chunks = array_chunk($post_ids, $chunk_size); foreach ($chunks as $chunk) { $texts = array(); $post_data = array(); foreach ($chunk as $post_id) { $content = AI_eShop_Chat_Content_Extractor::extract_content($post_id, $post_type, (bool)$deduplicated); if (empty($content)) { $results[$post_id] = array( 'success' => false, 'error' => __('No content to generate embedding', 'ai-eshop-optimizer') ); continue; } if ($embedding_provider === 'openai') { $content = $this->truncate_for_openai($content, 6000); } $texts[] = $content; $post_data[] = array( 'post_id' => $post_id, 'content_hash' => md5($content) ); } if (empty($texts)) { continue; } $texts_by_id = array_combine(array_column($post_data, 'post_id'), $texts); $api = ($embedding_provider === 'openai') ? AI_eShop_Chat_OpenAI_API::get_instance() : AI_eShop_Chat_Anthropic_API::get_instance(); $embeddings = null; $last_error = null; $delays = array(0, 1, 3, 9); foreach ($delays as $attempt_index => $delay_seconds) { if ($delay_seconds > 0) { sleep($delay_seconds); } $embeddings = $api->generate_embeddings_batch($texts_by_id, $model, AIEO_AI_Sources::EMB_INDEX); if (!is_wp_error($embeddings)) { break; } $last_error = $embeddings; $attempt_num = $attempt_index + 1; error_log(sprintf( 'AIEO embeddings batch retry %d/%d (model=%s, chunk_size=%d): %s', $attempt_num, count($delays), $model, count($texts_by_id), $embeddings->get_error_message() )); } if (is_wp_error($embeddings)) { $err_msg = $last_error ? $last_error->get_error_message() : 'unknown error'; foreach ($chunk as $post_id) { if (!isset($results[$post_id])) { $results[$post_id] = array( 'success' => false, 'error' => $err_msg ); } } continue; } $db = AI_eShop_Chat_Database_Manager::get_instance(); foreach ($post_data as $index => $data) { $post_id = $data['post_id']; if (isset($embeddings[$post_id])) { $stored = $db->store_embedding( $data['post_id'], $embeddings[$post_id], $data['content_hash'], $post_type, $model, $deduplicated ); $results[$data['post_id']] = array( 'success' => $stored, 'error' => $stored ? null : __('Failed to store embedding', 'ai-eshop-optimizer') ); } else { $results[$data['post_id']] = array( 'success' => false, 'error' => __('Missing embedding in response', 'ai-eshop-optimizer') ); } } } return $results; } public function generate_all_embeddings($post_type = 'product', $batch_size = 50, $offset = 0, $deduplicated = 0, $engine_type = null) { if (!AIEO_Chat_Indexing::enabled()) { return self::indexing_off_result(); } $db = AI_eShop_Chat_Database_Manager::get_instance(); if ($engine_type === null) { $provider = get_option('aieo_embedding_provider', 'voyage'); if ($provider === 'openai') { $engine_type = get_option('aieo_chat_openai_embedding_model', 'text-embedding-3-large'); } else { $engine_type = get_option('aieo_chat_embedding_model', 'voyage-2'); } } if ($post_type === 'product') { $needs_check_key = 'aieo_chat_needs_guard_checked'; if (get_transient($needs_check_key) === false) { global $wpdb; $needs_populated = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}aieo_core_product_vitals WHERE primary_need_id IS NOT NULL AND product_id = parent_id LIMIT 1" ); if ($needs_populated === 0) { if (!class_exists('AI_eShop_Product_Needs')) { require_once plugin_dir_path(dirname(__FILE__)) . 'class-product-needs.php'; } if (class_exists('AI_eShop_Product_Needs')) { AI_eShop_Product_Needs::get_instance()->populate_product_needs(); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO embedding trainer: vitals.primary_need was empty, re-ran PopulateProductNeeds.'); } } } set_transient($needs_check_key, 1, MINUTE_IN_SECONDS); } } global $wpdb; if ($post_type === 'product') { $vitals_helper = AI_eShop_Chat_Product_Vitals_Helper::get_instance(); if ($vitals_helper->is_vitals_table_available()) { $total_items = (int) $vitals_helper->count_sellable_units(array('in_stock_only' => true)); } else { $total_items = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'", $post_type )); } } else { $total_items = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'", $post_type )); } $exclude = $this->unindexable_posts($post_type); if (!empty($exclude) && $post_type !== 'product') { $total_items = max(0, $total_items - count($exclude)); } $post_ids = $db->get_posts_without_embeddings($post_type, $batch_size, $engine_type, $deduplicated, $exclude); if (empty($post_ids)) { return array( 'success' => true, 'message' => __('All posts have embeddings', 'ai-eshop-optimizer'), 'processed' => 0, 'remaining' => 0, 'total_items' => $total_items, 'total' => $total_items ); } $results = $this->generate_embeddings_batch($post_ids, $post_type, $deduplicated, $engine_type); $successes = count(array_filter($results, function($r) { return $r['success']; })); $remaining_count = $db->count_posts_without_embeddings($post_type, $engine_type, $deduplicated, $exclude); if ($remaining_count < 0) { $remaining_count = 0; } if ($remaining_count > $total_items) { $remaining_count = $total_items; } $failure_stats = $db->get_failure_statistics($post_type, $engine_type, $deduplicated); return array( 'success' => true, 'processed' => $successes, 'failed' => count($results) - $successes, 'remaining' => $remaining_count, 'total_items' => $total_items, 'total' => $total_items, 'failure_stats' => $failure_stats, 'details' => $results ); } public function generate_all_taxonomy_embeddings($taxonomy = 'product_cat', $batch_size = 50, $offset = 0, $deduplicated = 0) { if (!AIEO_Chat_Indexing::enabled()) { return self::indexing_off_result(); } $db = AI_eShop_Chat_Database_Manager::get_instance(); $embedding_provider = get_option('aieo_embedding_provider', 'voyage'); if ($embedding_provider === 'openai') { $engine_type = get_option('aieo_chat_openai_embedding_model', 'text-embedding-3-large'); } else { $engine_type = get_option('aieo_chat_embedding_model', 'voyage-2'); } $total_count = wp_count_terms(array( 'taxonomy' => $taxonomy, 'hide_empty' => false )); $term_ids = $db->get_terms_without_embeddings($taxonomy, $batch_size, $engine_type, $deduplicated); if (empty($term_ids)) { return array( 'success' => true, 'message' => __('All terms have embeddings', 'ai-eshop-optimizer'), 'processed' => 0, 'remaining' => 0, 'total' => $total_count ); } $results = $this->generate_taxonomy_embeddings_batch($term_ids, $taxonomy, $deduplicated); $successes = count(array_filter($results, function($r) { return $r['success']; })); $all_terms_without = $db->get_terms_without_embeddings($taxonomy, 999999, $engine_type, $deduplicated); $remaining_count = count($all_terms_without); if ($remaining_count < 0) { $remaining_count = 0; } if ($remaining_count > $total_count) { $remaining_count = $total_count; } return array( 'success' => true, 'processed' => $successes, 'failed' => count($results) - $successes, 'remaining' => $remaining_count, 'total' => $total_count, 'details' => $results ); } private function generate_taxonomy_embeddings_batch($term_ids, $taxonomy, $deduplicated = 0) { $db = AI_eShop_Chat_Database_Manager::get_instance(); $results = array(); wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_openai_embedding_model', 'options'); wp_cache_delete('aieo_chat_embedding_model', 'options'); $embedding_provider = get_option('aieo_embedding_provider', 'voyage'); if ($embedding_provider === 'openai') { $api = AI_eShop_Chat_OpenAI_API::get_instance(); $model = get_option('aieo_chat_openai_embedding_model', 'text-embedding-3-large'); } else { $api = AI_eShop_Chat_Anthropic_API::get_instance(); $model = get_option('aieo_chat_embedding_model', 'voyage-3-large'); } $contents_to_embed = array(); foreach ($term_ids as $term_id) { $content = AI_eShop_Chat_Content_Extractor::extract_content($term_id, 'taxonomy:' . $taxonomy, (bool)$deduplicated); $contents_to_embed[$term_id] = $content; } $embeddings = $api->generate_embeddings_batch($contents_to_embed, $model, AIEO_AI_Sources::EMB_INDEX); if (is_wp_error($embeddings)) { foreach ($term_ids as $term_id) { $results[] = array( 'term_id' => $term_id, 'success' => false, 'error' => $embeddings->get_error_message() ); } return $results; } foreach ($embeddings as $term_id => $embedding) { if (is_wp_error($embedding)) { $results[] = array( 'term_id' => $term_id, 'success' => false, 'error' => $embedding->get_error_message() ); continue; } $content = $contents_to_embed[$term_id]; $content_hash = md5($content); $engine_type = ($embedding_provider === 'openai') ? get_option('aieo_chat_openai_embedding_model', 'text-embedding-3-large') : get_option('aieo_chat_embedding_model', 'voyage-2'); $stored = $db->store_embedding($term_id, $embedding, $content_hash, 'taxonomy:' . $taxonomy, $engine_type, $deduplicated); $results[] = array( 'term_id' => $term_id, 'success' => $stored, 'error' => !$stored ? 'Failed to store embedding' : null ); } return $results; } public function register_index_hooks() { add_action('save_post_product', array($this, 'maybe_update_embedding'), 10, 2); add_action('delete_post', array($this, 'delete_embedding_on_delete')); add_action('save_post', array($this, 'maybe_update_learning_embedding'), 10, 2); add_action('rooster_lms/content_changed', array($this, 'learning_content_changed'), 10, 2); add_action('aieo_chat_generate_single_embedding', array(__CLASS__, 'run_queued_embedding'), 10, 2); } public static function run_queued_embedding($post_id, $post_type = '') { if (is_array($post_id)) { $post_type = isset($post_id['post_type']) ? (string) $post_id['post_type'] : $post_type; $post_id = isset($post_id['post_id']) ? $post_id['post_id'] : 0; } $post_id = (int) $post_id; if ($post_id <= 0) { return; } if (!AIEO_Chat_Indexing::enabled()) { return; } if ('' === (string) $post_type) { $post_type = (string) get_post_type($post_id); } self::get_instance()->generate_embedding($post_id, (string) $post_type); } private static function indexing_off_result() { return array( 'success' => false, 'indexing_off' => true, 'message' => AIEO_Chat_Indexing::refusal(), 'processed' => 0, 'remaining' => 0, 'total' => 0, 'total_items' => 0, ); } public function maybe_update_embedding($post_id, $post) { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (wp_is_post_revision($post_id)) { return; } if ($post->post_status !== 'publish') { return; } $enabled_types = class_exists('AIEO_Content_Types') ? AIEO_Content_Types::chat_post_types() : get_option('aieo_chat_enabled_post_types', array('product')); if (!in_array($post->post_type, $enabled_types, true)) { return; } if (class_exists('AIEO_Content_Types') && AIEO_Content_Types::is_learning_post_type($post->post_type) && !$this->is_indexable((int) $post_id)) { AI_eShop_Chat_Database_Manager::get_instance()->delete_embedding((int) $post_id); return; } if (!AIEO_Chat_Indexing::enabled()) { return; } wp_schedule_single_event(time() + 10, 'aieo_chat_generate_single_embedding', array( 'post_id' => $post_id, 'post_type' => $post->post_type )); } public function delete_embedding_on_delete($post_id) { $db = AI_eShop_Chat_Database_Manager::get_instance(); $db->delete_embedding($post_id); } public function maybe_update_learning_embedding($post_id, $post) { if (!$post instanceof WP_Post || !class_exists('AIEO_Content_Types') || !AIEO_Content_Types::is_learning_post_type($post->post_type)) { return; } $this->maybe_update_embedding($post_id, $post); } public function learning_content_changed($type, $id) { if (!in_array((string) $type, array('course', 'lesson', 'quiz'), true) || (int) $id <= 0) { return; } $post = get_post((int) $id); if (!$post || !class_exists('AIEO_Content_Types') || !AIEO_Content_Types::is_learning_post_type($post->post_type)) { return; } if ('publish' !== $post->post_status || !$this->is_indexable((int) $post->ID)) { AI_eShop_Chat_Database_Manager::get_instance()->delete_embedding((int) $post->ID); return; } if (!in_array($post->post_type, AIEO_Content_Types::chat_post_types(), true)) { return; } if (!AIEO_Chat_Indexing::enabled()) { return; } wp_schedule_single_event(time() + 10, 'aieo_chat_generate_single_embedding', array( 'post_id' => (int) $post->ID, 'post_type' => $post->post_type, )); } public function is_indexable($post_id) { $ok = (array) apply_filters('rooster/learning/indexable_posts', array((int) $post_id)); return in_array((int) $post_id, array_map('intval', $ok), true); } public function unindexable_posts($post_type) { if (!class_exists('AIEO_Content_Types') || !AIEO_Content_Types::is_learning_post_type($post_type)) { return array(); } $all = array_map('intval', (array) get_posts(array( 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, ))); if (empty($all)) { return array(); } $ok = array_map('intval', (array) apply_filters('rooster/learning/indexable_posts', $all)); $out = array_values(array_diff($all, $ok)); if (!empty($out)) { $db = AI_eShop_Chat_Database_Manager::get_instance(); foreach ($out as $pid) { $db->delete_embedding($pid); } } return $out; } public function cosine_similarity($vec1, $vec2) { $dot_product = 0; $magnitude1 = 0; $magnitude2 = 0; $len = min(count($vec1), count($vec2)); for ($i = 0; $i < $len; $i++) { $dot_product += $vec1[$i] * $vec2[$i]; $magnitude1 += $vec1[$i] * $vec1[$i]; $magnitude2 += $vec2[$i] * $vec2[$i]; } $magnitude1 = sqrt($magnitude1); $magnitude2 = sqrt($magnitude2); if ($magnitude1 == 0 || $magnitude2 == 0) { return 0; } return $dot_product / ($magnitude1 * $magnitude2); } public function search_similar($query, $args = array()) { $defaults = array( 'post_types' => array('product'), 'limit' => 10, 'min_similarity' => 0.5 ); $args = wp_parse_args($args, $defaults); $embedding_provider = get_option('aieo_embedding_provider', 'voyage'); if ($embedding_provider === 'openai') { $model = get_option('aieo_chat_openai_embedding_model', 'text-embedding-3-large'); $api = AI_eShop_Chat_OpenAI_API::get_instance(); $query_embedding = $api->generate_embedding($query, $model); } else { $model = get_option('aieo_chat_embedding_model', 'voyage-2'); $api = AI_eShop_Chat_Anthropic_API::get_instance(); $query_embedding = $api->generate_embedding($query, $model, AIEO_AI_Sources::EMB_CHAT); } if (is_wp_error($query_embedding)) { return $query_embedding; } $db = AI_eShop_Chat_Database_Manager::get_instance(); $embeddings = $db->get_embeddings_for_search($args['post_types'], null, $model); if (empty($embeddings)) { return array(); } $similarities = array(); foreach ($embeddings as $post_id => $embedding) { $similarity = $this->cosine_similarity($query_embedding, $embedding); if ($similarity >= $args['min_similarity']) { $similarities[$post_id] = $similarity; } } arsort($similarities); $similarities = $this->filter_by_category($similarities, isset($args['category']) ? $args['category'] : ''); $similarities = array_slice($similarities, 0, $args['limit'], true); $results = array(); foreach ($similarities as $post_id => $score) { $results[] = array( 'post_id' => $post_id, 'score' => $score, 'post' => get_post($post_id) ); } return $results; } private function get_excluded_term_keys() { if (get_option('aieo_chat_honor_navigator_hidden_terms', '1') !== '1') { return array(); } if (!class_exists('AIEO_DMM_Settings')) { return array(); } $raw = (string) AIEO_DMM_Settings::get('navigator_excluded_terms', ''); if ($raw === '') { return array(); } $aliases = array( 'product_brand' => array('product_brand', 'pwb-brand', 'pa_brand', 'pa-brand'), 'pwb-brand' => array('product_brand', 'pwb-brand', 'pa_brand', 'pa-brand'), 'pa_brand' => array('product_brand', 'pwb-brand', 'pa_brand', 'pa-brand'), 'pa-brand' => array('product_brand', 'pwb-brand', 'pa_brand', 'pa-brand'), 'pa-need' => array('pa-need', 'pa_need'), 'pa_need' => array('pa-need', 'pa_need'), ); $out = array(); foreach (preg_split('/[,\s]+/', $raw) as $tok) { $tok = trim($tok); if ($tok === '') continue; if (strpos($tok, ':') === false) { $out[] = 'product_cat:' . (int) $tok; continue; } list($tax, $id) = explode(':', $tok, 2); $tax = sanitize_key($tax); $id = (int) $id; $taxes = isset($aliases[$tax]) ? $aliases[$tax] : array($tax); foreach ($taxes as $alias) { $out[] = $alias . ':' . $id; } } return array_flip(array_unique($out)); } private function get_excluded_product_ids() { static $cache = null; if ($cache !== null) { return $cache; } $cache = array(); $excluded_keys = $this->get_excluded_term_keys(); if (empty($excluded_keys)) { return $cache; } $by_tax = array(); foreach (array_keys($excluded_keys) as $key) { $parts = explode(':', $key, 2); if (count($parts) !== 2) continue; $by_tax[$parts[0]][] = (int) $parts[1]; } global $wpdb; foreach ($by_tax as $tax => $ids) { $ids = array_unique(array_filter($ids)); if (empty($ids)) continue; $placeholders = implode(',', array_fill(0, count($ids), '%d')); $tt_ids = $wpdb->get_col($wpdb->prepare( "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s AND term_id IN ($placeholders)", array_merge(array($tax), $ids) )); if (empty($tt_ids)) continue; $tt_placeholders = implode(',', array_fill(0, count($tt_ids), '%d')); $rows = $wpdb->get_col($wpdb->prepare( "SELECT DISTINCT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ($tt_placeholders)", $tt_ids )); foreach ($rows as $pid) { $cache[(int) $pid] = true; } } return $cache; } private function filter_by_category($similarities, $category) { global $wpdb; $excluded_pids = $this->get_excluded_product_ids(); if (!empty($excluded_pids)) { $kept = array(); foreach ($similarities as $pid => $score) { if (isset($excluded_pids[(int) $pid])) continue; if (get_post_type($pid) === 'product_variation') { $parent = (int) wp_get_post_parent_id($pid); if ($parent && isset($excluded_pids[$parent])) continue; } $kept[$pid] = $score; } $similarities = $kept; } if (empty($similarities) || empty($category)) { return $similarities; } $post_ids = array_keys($similarities); $non_product_similarities = array(); $hierarchy_lookup = array(); $hierarchy_ids = array(); foreach ($post_ids as $post_id) { $post_type = get_post_type($post_id); if ($post_type === 'product' || $post_type === 'product_variation') { if ($post_type === 'product_variation') { $parent_id = (int) wp_get_post_parent_id($post_id); $lookup_id = $parent_id > 0 ? $parent_id : $post_id; } else { $lookup_id = (int) $post_id; } $hierarchy_lookup[$lookup_id][] = $post_id; $hierarchy_ids[$lookup_id] = true; } else { $non_product_similarities[$post_id] = $similarities[$post_id]; } } if (empty($hierarchy_ids)) { return $similarities; } $hierarchy_id_list = array_keys($hierarchy_ids); $placeholders = implode(',', array_fill(0, count($hierarchy_id_list), '%d')); $query = $wpdb->prepare( "SELECT DISTINCT product_id FROM {$wpdb->prefix}aieo_core_product_category_hierarchy WHERE product_id IN ($placeholders) AND top_category = %s", array_merge($hierarchy_id_list, array($category)) ); $matching_hierarchy_ids_raw = $wpdb->get_col($query); $keep_post_ids = array(); foreach ($matching_hierarchy_ids_raw as $hierarchy_id) { $hierarchy_id = (int) $hierarchy_id; if (!empty($hierarchy_lookup[$hierarchy_id])) { foreach ($hierarchy_lookup[$hierarchy_id] as $original_post_id) { $keep_post_ids[$original_post_id] = true; } } } $filtered_similarities = array(); foreach ($similarities as $post_id => $score) { if (isset($non_product_similarities[$post_id]) || isset($keep_post_ids[$post_id])) { $filtered_similarities[$post_id] = $score; } } return $filtered_similarities; } private function sort_by_sales($post_ids) { global $wpdb; if (empty($post_ids)) { return $post_ids; } $priority_metric = get_option('aieo_chat_product_priority_metric', 'total_sales'); $metric_column_map = array( 'total_sales' => 'TotalItemSales', 'distinct_orders' => 'DistinctOrderSales', 'total_turnover' => 'TotalTurnover', 'unique_sales' => 'UniqueItemSales', 'first_in_cart' => 'FirstItemSales', 'last_in_cart' => 'LastItemSales', 'avg_jlw' => 'AvgJLW', 'avg_vlw' => 'AvgVLW' ); $column = isset($metric_column_map[$priority_metric]) ? $metric_column_map[$priority_metric] : 'TotalItemSales'; $vitals_table = $wpdb->prefix . 'aieo_core_product_vitals'; $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $vitals_table)) === $vitals_table; if (!$table_exists) { $products_with_sales = array(); foreach ($post_ids as $post_id) { $total_sales = (int) get_post_meta($post_id, 'total_sales', true); $products_with_sales[] = array( 'id' => $post_id, 'sales' => $total_sales ); } usort($products_with_sales, function($a, $b) { return $b['sales'] - $a['sales']; }); return array_column($products_with_sales, 'id'); } $placeholders = implode(',', array_fill(0, count($post_ids), '%d')); $temp_orders_table = $wpdb->prefix . 'aieo_temp_orders'; $query = " SELECT ProductId, MAX({$column}) as metric_value FROM {$temp_orders_table} WHERE ProductId IN ({$placeholders}) GROUP BY ProductId ORDER BY metric_value DESC "; $prepared_query = $wpdb->prepare($query, $post_ids); $results = $wpdb->get_results($prepared_query, ARRAY_A); if (empty($results)) { return $post_ids; } $sorted_ids = array_column($results, 'ProductId'); $missing_ids = array_diff($post_ids, $sorted_ids); if (!empty($missing_ids)) { $sorted_ids = array_merge($sorted_ids, $missing_ids); } $sorted_ids = array_values(array_unique(array_map('intval', $sorted_ids))); return $sorted_ids; } public function ajax_generate_embeddings() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } if (!AIEO_Chat_Indexing::enabled()) { wp_send_json_error(array('message' => AIEO_Chat_Indexing::refusal(), 'indexing_off' => true)); } $post_type = isset($_POST['post_type']) ? sanitize_text_field(wp_unslash( $_POST['post_type'] )) : 'product'; $batch_size = isset($_POST['batch_size']) ? (int) $_POST['batch_size'] : 50; $offset = isset($_POST['offset']) ? (int) $_POST['offset'] : 0; $engine_type = isset($_POST['engine_type']) ? sanitize_text_field(wp_unslash( $_POST['engine_type'] )) : null; $deduplicated = isset($_POST['deduplicated']) ? (int) $_POST['deduplicated'] : 0; $original_provider = null; $original_model = null; if ($engine_type) { if (strpos($engine_type, 'text-embedding') === 0) { $original_provider = get_option('aieo_embedding_provider'); $original_model = get_option('aieo_chat_openai_embedding_model'); update_option('aieo_embedding_provider', 'openai'); update_option('aieo_chat_openai_embedding_model', $engine_type); wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_openai_embedding_model', 'options'); } else { $original_provider = get_option('aieo_embedding_provider'); $original_model = get_option('aieo_chat_embedding_model'); update_option('aieo_embedding_provider', 'voyage'); update_option('aieo_chat_embedding_model', $engine_type); wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_embedding_model', 'options'); } } $result = $this->generate_all_embeddings($post_type, $batch_size, $offset, $deduplicated, $engine_type); if ($engine_type && $original_provider !== null) { update_option('aieo_embedding_provider', $original_provider); if (strpos($engine_type, 'text-embedding') === 0) { update_option('aieo_chat_openai_embedding_model', $original_model); } else { update_option('aieo_chat_embedding_model', $original_model); } wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_openai_embedding_model', 'options'); wp_cache_delete('aieo_chat_embedding_model', 'options'); } wp_send_json_success($result); } public function ajax_generate_taxonomy_embeddings() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } if (!AIEO_Chat_Indexing::enabled()) { wp_send_json_error(array('message' => AIEO_Chat_Indexing::refusal(), 'indexing_off' => true)); } $taxonomy = isset($_POST['taxonomy']) ? sanitize_text_field(wp_unslash( $_POST['taxonomy'] )) : 'product_cat'; $batch_size = isset($_POST['batch_size']) ? (int) $_POST['batch_size'] : 50; $offset = isset($_POST['offset']) ? (int) $_POST['offset'] : 0; $engine_type = isset($_POST['engine_type']) ? sanitize_text_field(wp_unslash( $_POST['engine_type'] )) : null; $deduplicated = isset($_POST['deduplicated']) ? (int) $_POST['deduplicated'] : 0; $original_provider = null; $original_model = null; if ($engine_type) { if (strpos($engine_type, 'text-embedding') === 0) { $original_provider = get_option('aieo_embedding_provider'); $original_model = get_option('aieo_chat_openai_embedding_model'); update_option('aieo_embedding_provider', 'openai'); update_option('aieo_chat_openai_embedding_model', $engine_type); wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_openai_embedding_model', 'options'); } else { $original_provider = get_option('aieo_embedding_provider'); $original_model = get_option('aieo_chat_embedding_model'); update_option('aieo_embedding_provider', 'voyage'); update_option('aieo_chat_embedding_model', $engine_type); wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_embedding_model', 'options'); } } $result = $this->generate_all_taxonomy_embeddings($taxonomy, $batch_size, $offset, $deduplicated); if ($engine_type && $original_provider !== null) { update_option('aieo_embedding_provider', $original_provider); if (strpos($engine_type, 'text-embedding') === 0) { update_option('aieo_chat_openai_embedding_model', $original_model); } else { update_option('aieo_chat_embedding_model', $original_model); } wp_cache_delete('aieo_embedding_provider', 'options'); wp_cache_delete('aieo_chat_openai_embedding_model', 'options'); wp_cache_delete('aieo_chat_embedding_model', 'options'); } wp_send_json_success($result); } public function ajax_regenerate_embedding() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } if (!AIEO_Chat_Indexing::enabled()) { wp_send_json_error(array('message' => AIEO_Chat_Indexing::refusal(), 'indexing_off' => true)); } $post_id = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0; if (!$post_id) { wp_send_json_error(array('message' => __('Invalid post ID', 'ai-eshop-optimizer'))); } $post = get_post($post_id); if (!$post) { wp_send_json_error(array('message' => __('Post not found', 'ai-eshop-optimizer'))); } $result = $this->generate_embedding($post_id, $post->post_type, true); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } wp_send_json_success(array('message' => __('Embedding regenerated successfully', 'ai-eshop-optimizer'))); } public function ajax_delete_embeddings() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } global $wpdb; $table_name = $wpdb->prefix . 'aieo_chat_embeddings'; $delete_all = isset($_POST['delete_all']) && sanitize_text_field( wp_unslash( $_POST['delete_all'] ) ); $types = isset($_POST['types']) && is_array( wp_unslash( $_POST['types'] ) ) ? array_map('sanitize_text_field', wp_unslash( $_POST['types'] )) : array(); $engine_type = isset($_POST['engine_type']) ? sanitize_text_field(wp_unslash( $_POST['engine_type'] )) : ''; $deduplicated = isset($_POST['deduplicated']) ? intval($_POST['deduplicated']) : null; if ($delete_all) { if ($engine_type) { $where = "engine_type = %s"; $prepare_args = array($engine_type); if ($deduplicated !== null) { $where .= " AND deduplicated = %d"; $prepare_args[] = $deduplicated; } $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE {$where}", $prepare_args )); $result = $wpdb->query($wpdb->prepare( "DELETE FROM {$table_name} WHERE {$where}", $prepare_args )); if ($result === false) { wp_send_json_error(array('message' => __('Failed to delete embeddings', 'ai-eshop-optimizer'))); } $dedup_msg = $deduplicated !== null ? ($deduplicated === 1 ? ' (deduplicated)' : ' (full content)') : ''; wp_send_json_success(array( 'message' => sprintf(__('Successfully deleted all %1$d embeddings for engine: %2$s%3$s', 'ai-eshop-optimizer'), $count, $engine_type, $dedup_msg) )); } else { $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}"); $result = $wpdb->query("TRUNCATE TABLE {$table_name}"); if ($result === false) { wp_send_json_error(array('message' => __('Failed to delete embeddings', 'ai-eshop-optimizer'))); } wp_send_json_success(array( 'message' => sprintf(__('Successfully deleted all %d embeddings', 'ai-eshop-optimizer'), $count) )); } } elseif (!empty($types)) { $placeholders = implode(',', array_fill(0, count($types), '%s')); if ($engine_type) { $where = "post_type IN ($placeholders) AND engine_type = %s"; $prepare_args = array_merge($types, array($engine_type)); if ($deduplicated !== null) { $where .= " AND deduplicated = %d"; $prepare_args[] = $deduplicated; } $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE {$where}", $prepare_args )); $result = $wpdb->query($wpdb->prepare( "DELETE FROM {$table_name} WHERE {$where}", $prepare_args )); } else { $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE post_type IN ($placeholders)", $types )); $result = $wpdb->query($wpdb->prepare( "DELETE FROM {$table_name} WHERE post_type IN ($placeholders)", $types )); } if ($result === false) { wp_send_json_error(array('message' => __('Failed to delete embeddings', 'ai-eshop-optimizer'))); } $type_labels = array( 'product' => __('Products', 'ai-eshop-optimizer'), 'product_variation' => __('Product Variations', 'ai-eshop-optimizer'), 'post' => __('Blog Posts', 'ai-eshop-optimizer'), 'page' => __('Pages', 'ai-eshop-optimizer') ); $deleted_types = array(); foreach ($types as $type) { $deleted_types[] = isset($type_labels[$type]) ? $type_labels[$type] : ucfirst($type); } $type_names = implode(', ', $deleted_types); $engine_msg = $engine_type ? ' (' . $engine_type : ''; if ($deduplicated !== null) { $engine_msg .= $deduplicated === 1 ? ' - deduplicated' : ' - full content'; } $engine_msg .= $engine_type ? ')' : ''; wp_send_json_success(array( 'message' => sprintf( __('Successfully deleted %1$d embeddings for: %2$s%3$s', 'ai-eshop-optimizer'), $count, $type_names, $engine_msg ) )); } else { wp_send_json_error(array('message' => __('No content types selected', 'ai-eshop-optimizer'))); } } public function ajax_get_engine_stats() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } $engine_type = isset($_POST['engine_type']) ? sanitize_text_field(wp_unslash( $_POST['engine_type'] )) : ''; $deduplicated = isset($_POST['deduplicated']) ? intval($_POST['deduplicated']) : 1; if (empty($engine_type)) { wp_send_json_error(array('message' => __('No engine type specified', 'ai-eshop-optimizer'))); } global $wpdb; $db = AI_eShop_Chat_Database_Manager::get_instance(); $stats = array(); $all_post_types = get_post_types(array('public' => true), 'objects'); $vitals_helper = AI_eShop_Chat_Product_Vitals_Helper::get_instance(); $vitals_available = $vitals_helper->is_vitals_table_available(); $vitals_table = $wpdb->prefix . 'aieo_core_product_vitals'; foreach ($all_post_types as $post_type_obj) { $post_type = $post_type_obj->name; if (in_array($post_type, array('attachment', 'revision', 'nav_menu_item'))) { continue; } if ($post_type === 'product' && $vitals_available) { $total_count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT v.product_id) FROM {$vitals_table} v INNER JOIN {$wpdb->posts} p ON v.product_id = p.ID LEFT JOIN ( SELECT DISTINCT parent_id FROM {$vitals_table} WHERE product_id <> parent_id ) vp ON vp.parent_id = v.product_id WHERE p.post_type IN ('product', 'product_variation') AND p.post_status = 'publish' AND v.stock > 0 AND (v.product_id <> v.parent_id OR vp.parent_id IS NULL)" ); } else { $counts = wp_count_posts($post_type); $total_count = isset($counts->publish) ? (int) $counts->publish : 0; } if ($total_count === 0) { continue; } if ($post_type === 'product' && $vitals_available) { $has_count = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(DISTINCT e.post_id) FROM {$wpdb->prefix}aieo_chat_embeddings e INNER JOIN {$wpdb->posts} p ON e.post_id = p.ID INNER JOIN {$vitals_table} v ON e.post_id = v.product_id LEFT JOIN ( SELECT DISTINCT parent_id FROM {$vitals_table} WHERE product_id <> parent_id ) vp ON vp.parent_id = v.product_id WHERE e.engine_type = %s AND e.deduplicated = %d AND p.post_type IN ('product', 'product_variation') AND p.post_status = 'publish' AND v.stock > 0 AND (v.product_id <> v.parent_id OR vp.parent_id IS NULL)", $engine_type, $deduplicated )); } else { $has_count = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(DISTINCT e.post_id) FROM {$wpdb->prefix}aieo_chat_embeddings e INNER JOIN {$wpdb->posts} p ON e.post_id = p.ID WHERE e.post_type = %s AND e.engine_type = %s AND e.deduplicated = %d AND p.post_type = %s AND p.post_status = 'publish'", $post_type, $engine_type, $deduplicated, $post_type )); } $stats[$post_type] = array( 'label' => $post_type_obj->label, 'total' => $total_count, 'has' => $has_count, 'missing' => $total_count - $has_count ); } wp_send_json_success($stats); } public function ajax_update_embedding_model() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } $engine_type = isset($_POST['engine_type']) ? sanitize_text_field(wp_unslash( $_POST['engine_type'] )) : ''; if (empty($engine_type)) { wp_send_json_error(array('message' => __('No engine type specified', 'ai-eshop-optimizer'))); } $registry = AI_eShop_Chat_Model_Registry::get_instance(); $is_openai = $registry->is_valid_model($engine_type, 'openai'); if (!$is_openai && !$registry->is_valid_model($engine_type, 'voyage')) { wp_send_json_error(array('message' => __('Invalid engine type', 'ai-eshop-optimizer'))); } update_option('aieo_embedding_provider', $is_openai ? 'openai' : 'voyage'); if ($is_openai) { update_option('aieo_chat_openai_embedding_model', $engine_type); } else { update_option('aieo_chat_embedding_model', $engine_type); } wp_send_json_success(array( 'message' => sprintf(__('Embedding model updated to: %s', 'ai-eshop-optimizer'), $engine_type) )); } public function ajax_get_available_engines() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } global $wpdb; $table = $wpdb->prefix . 'aieo_chat_embeddings'; $results = $wpdb->get_results( "SELECT DISTINCT engine_type, deduplicated FROM {$table} ORDER BY engine_type, deduplicated", ARRAY_A ); if ($results === false) { wp_send_json_error(array('message' => __('Database error', 'ai-eshop-optimizer'))); } $voyage_engines = array(); $openai_engines = array(); $openai_models = array('text-embedding-3-large', 'text-embedding-3-small', 'text-embedding-ada-002'); foreach ($results as $row) { $engine = $row['engine_type']; $dedup = intval($row['deduplicated']); $label = $engine . ($dedup === 1 ? ' (deduplicated)' : ' (full content)'); $value = $engine . ':' . $dedup; $engine_data = array( 'label' => $label, 'value' => $value, 'engine' => $engine, 'deduplicated' => $dedup ); if (in_array($engine, $openai_models)) { $openai_engines[] = $engine_data; } else { $voyage_engines[] = $engine_data; } } wp_send_json_success(array( 'voyage' => $voyage_engines, 'openai' => $openai_engines )); } public function ajax_get_delete_stats() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } global $wpdb; $table = $wpdb->prefix . 'aieo_chat_embeddings'; $filter = isset($_POST['filter']) ? sanitize_text_field(wp_unslash( $_POST['filter'] )) : ''; $engine_type = null; $deduplicated = null; if (!empty($filter)) { $parts = explode(':', $filter); if (count($parts) === 2) { $engine_type = $parts[0]; $deduplicated = intval($parts[1]); } } $where = '1=1'; $prepare_args = array(); if ($engine_type) { $where .= ' AND engine_type = %s'; $prepare_args[] = $engine_type; } if ($deduplicated !== null) { $where .= ' AND deduplicated = %d'; $prepare_args[] = $deduplicated; } if (!empty($prepare_args)) { $results = $wpdb->get_results($wpdb->prepare( "SELECT post_type, COUNT(*) as count FROM {$table} WHERE {$where} GROUP BY post_type ORDER BY post_type", $prepare_args ), ARRAY_A); } else { $results = $wpdb->get_results( "SELECT post_type, COUNT(*) as count FROM {$table} WHERE {$where} GROUP BY post_type ORDER BY post_type", ARRAY_A ); } if (!empty($prepare_args)) { $total = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$where}", $prepare_args )); } else { $total = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE {$where}"); } $by_type = array(); foreach ($results as $row) { $by_type[$row['post_type']] = intval($row['count']); } wp_send_json_success(array( 'total' => intval($total), 'by_type' => $by_type )); } public function ajax_get_delete_counts() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer'))); } global $wpdb; $table_name = $wpdb->prefix . 'aieo_chat_embeddings'; $engine_type = isset($_POST['engine_type']) ? sanitize_text_field(wp_unslash( $_POST['engine_type'] )) : ''; $counts = array(); if ($engine_type) { $results = $wpdb->get_results($wpdb->prepare( "SELECT post_type, COUNT(*) as count FROM {$table_name} WHERE engine_type = %s GROUP BY post_type", $engine_type ), ARRAY_A); } else { $results = $wpdb->get_results( "SELECT post_type, COUNT(*) as count FROM {$table_name} GROUP BY post_type", ARRAY_A ); } foreach ($results as $row) { $counts[$row['post_type']] = (int) $row['count']; } $common_types = array('product', 'product_variation', 'post', 'page'); foreach ($common_types as $type) { if (!isset($counts[$type])) { $counts[$type] = 0; } } wp_send_json_success($counts); } private function truncate_for_openai($content, $max_tokens = 6000) { $max_chars = $max_tokens * 2; if (strlen($content) <= $max_chars) { return $content; } $truncated = substr($content, 0, $max_chars); $last_space = strrpos($truncated, ' '); if ($last_space !== false && $last_space > ($max_chars * 0.9)) { $truncated = substr($truncated, 0, $last_space); } return $truncated; } public function ajax_save_last_trained_engine() { check_ajax_referer('aieo_chat_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Unauthorized', 'ai-eshop-optimizer')); return; } $engine_type = isset($_POST['engine_type']) ? sanitize_text_field(wp_unslash( $_POST['engine_type'] )) : ''; $deduplicated = isset($_POST['deduplicated']) ? intval($_POST['deduplicated']) : null; if (empty($engine_type)) { wp_send_json_error(__('Engine type is required', 'ai-eshop-optimizer')); return; } update_option('aieo_chat_last_trained_engine', $engine_type); if ($deduplicated !== null) { update_option('aieo_chat_last_trained_deduplication', $deduplicated); } wp_send_json_success(array( 'message' => __('Last trained settings saved', 'ai-eshop-optimizer'), 'engine_type' => $engine_type, 'deduplicated' => $deduplicated )); } } add_action('aieo_chat_generate_single_embedding', array('AI_eShop_Chat_Embedding_Manager', 'run_queued_embedding'), 10, 2);