get_conversation_messages($conversation_id, 5); } $retrieval_query = $message; if (!empty($history)) { $prior_questions = array(); foreach ($history as $h) { $q = isset($h['user_message']) ? trim((string) $h['user_message']) : ''; if ('' !== $q) { $prior_questions[] = $q; } } $prior_questions = array_slice($prior_questions, -3); if (!empty($prior_questions)) { $retrieval_query = implode("\n", $prior_questions) . "\n" . $message; } } $embedding_manager = AI_eShop_Chat_Embedding_Manager::get_instance(); $search_args = array( 'post_types' => AIEO_Content_Types::chat_post_types(), 'limit' => 50, 'min_similarity' => 0.3 ); if (!empty($args['category']) && get_option('aieo_chat_category_filter_enabled', '0') === '1') { $search_args['category'] = $args['category']; } $search_results = $embedding_manager->search_similar($retrieval_query, $search_args); if (is_wp_error($search_results)) { return $this->process_simple_chat($message, $conversation_id); } $filtered_results = $this->filter_results_by_quotas($search_results); $context = $this->build_context_from_results($filtered_results); $system_prompt = $this->build_system_prompt($context); if (!empty($args['category']) && get_option('aieo_chat_category_filter_enabled', '0') === '1') { $system_prompt .= "\n\nIMPORTANT: The user has selected the '{$args['category']}' category. You must ONLY recommend products from this category. Do not suggest products from other categories."; } $custom_prompt = get_option('aieo_chat_custom_prompt', ''); if (!empty($custom_prompt)) { $system_prompt .= "\n\nAdditional Instructions:\n" . $custom_prompt; } $messages = array(); foreach ($history as $h) { $u = isset($h['user_message']) ? trim((string) $h['user_message']) : ''; $a = isset($h['assistant_message']) ? trim((string) $h['assistant_message']) : ''; if ('' !== $u && '' !== $a) { $messages[] = array('role' => 'user', 'content' => $u); $messages[] = array('role' => 'assistant', 'content' => $a); } } $messages[] = array( 'role' => 'user', 'content' => $message ); $chat_provider = get_option('aieo_chat_provider', 'anthropic'); if ($chat_provider === 'openai') { $api = AI_eShop_Chat_OpenAI_API::get_instance(); $response = $api->chat_completion($messages, array( 'system' => $system_prompt, 'model' => get_option('aieo_chat_openai_chat_model', 'gpt-4o'), 'max_tokens' => (int) get_option('aieo_chat_max_tokens', 2000), 'temperature' => (float) get_option('aieo_chat_temperature', 0.5) )); } else { $api = AI_eShop_Chat_Anthropic_API::get_instance(); $response = $api->chat_completion($messages, array( 'system' => $system_prompt, 'model' => get_option('aieo_chat_model', 'claude-3-5-sonnet-20241022'), 'max_tokens' => (int) get_option('aieo_chat_max_tokens', 2000), 'temperature' => (float) get_option('aieo_chat_temperature', 0.5), 'source' => AIEO_AI_Sources::CHAT_CLIENT )); } if (is_wp_error($response)) { return $response; } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Chat API Response: ' . print_r($response, true)); } $assistant_message = isset($response['content'][0]['text']) ? $response['content'][0]['text'] : ''; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Extracted Message: ' . $assistant_message); } if (empty($assistant_message)) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Message extraction failed. Response structure: ' . print_r($response, true)); } return new WP_Error('empty_response', __('No response from AI', 'ai-eshop-optimizer')); } $was_truncated = false; if (isset($response['stop_reason']) && $response['stop_reason'] === 'max_tokens') { $was_truncated = true; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Warning: Response was truncated due to max_tokens limit'); } } elseif (isset($response['choices'][0]['finish_reason']) && $response['choices'][0]['finish_reason'] === 'length') { $was_truncated = true; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Warning: Response was truncated due to max_tokens limit (OpenAI)'); } } $tokens_in = (int) ($response['usage']['input_tokens'] ?? $response['usage']['prompt_tokens'] ?? 0); $tokens_out = (int) ($response['usage']['output_tokens'] ?? $response['usage']['completion_tokens'] ?? 0); $stop_reason = (string) ($response['stop_reason'] ?? ($response['choices'][0]['finish_reason'] ?? '')); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Token usage: in=' . $tokens_in . ' out=' . $tokens_out . ' stop=' . $stop_reason); } if ($was_truncated) { $assistant_message = preg_replace('/\[([^\]]*?)(\].*)?$/', '', $assistant_message); $assistant_message = rtrim($assistant_message); $continuation_msg = "\n\n" . __('You can see more products below, or ask me a more specific question so I can help you better.', 'ai-eshop-optimizer'); $assistant_message .= $continuation_msg; } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Step 7: Getting content data...'); } $post_ids = $this->select_recommendation_cards($assistant_message, $filtered_results); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Filtered post IDs: ' . print_r($post_ids, true)); } $products = AI_eShop_Chat_Content_Extractor::get_posts_data($post_ids); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Products retrieved: ' . count($products)); } $embedding_provider = get_option('aieo_embedding_provider', 'voyage'); $embedding_model = ($embedding_provider === 'openai') ? get_option('aieo_chat_openai_embedding_model', 'text-embedding-3-large') : get_option('aieo_chat_embedding_model', 'voyage-2'); $dedup_mode = get_option('aieo_chat_deduplication_mode', 1); $embedder_used = $embedding_model . ':' . $dedup_mode; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Embedder constructed: ' . $embedder_used); } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Building potential suggestions summary...'); } $products_raw = array(); $posts_raw = array(); foreach ($search_results as $result) { $post = get_post($result['post_id']); if (!$post) continue; if ('publish' !== $post->post_status || '' !== (string) $post->post_password) continue; $result_with_title = array( 'post_id' => $result['post_id'], 'title' => $post->post_title, 'score' => round($result['score'] * 100, 2), 'post_type' => $post->post_type ); if ($post->post_type === 'product' || $post->post_type === 'product_variation') { $products_raw[] = $result_with_title; } else { $posts_raw[] = $result_with_title; } } $potential_suggestions = array( 'products' => array_slice($products_raw, 0, 5), 'posts_pages' => array_slice($posts_raw, 0, 5), 'total_found' => count($search_results) ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Potential suggestions: ' . count($products_raw) . ' products, ' . count($posts_raw) . ' posts/pages'); } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO About to save chat history...'); } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Embedder used: ' . $embedder_used); } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Post IDs count: ' . count($post_ids)); } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Potential suggestions count: ' . count($potential_suggestions)); } $db = AI_eShop_Chat_Database_Manager::get_instance(); $chat_model = ($chat_provider === 'openai') ? get_option('aieo_chat_openai_chat_model', 'gpt-4o') : get_option('aieo_chat_model', 'claude-3-5-sonnet-20241022'); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Chat model: ' . $chat_model); } $chat_id = $db->save_chat_message( $conversation_id, $message, $assistant_message, $chat_model, $post_ids, $embedder_used, $potential_suggestions, $tokens_in > 0 ? $tokens_in : null, $tokens_out > 0 ? $tokens_out : null, $stop_reason !== '' ? $stop_reason : null ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO save_chat_message call completed. Chat ID: ' . ($chat_id ? $chat_id : 'false')); } $hide_frontend_products = get_option('aieo_chat_hide_frontend_products', '0') === '1'; $response = array( 'message' => $assistant_message, 'products' => $hide_frontend_products ? array() : $products, 'conversation_id' => $conversation_id ); if ($chat_id) { $response['chat_id'] = $chat_id; } return $response; } private function rerank_with_business_logic($results) { if (empty($results)) { return $results; } $rerank_enabled = get_option('aieo_chat_rerank_enabled', '1'); if ($rerank_enabled !== '1') { return $results; } $weight_variations_2_10 = floatval(get_option('aieo_chat_rerank_variations_2_10', '1.10')); $weight_variations_10_50 = floatval(get_option('aieo_chat_rerank_variations_10_50', '1.30')); $weight_variations_50_plus = floatval(get_option('aieo_chat_rerank_variations_50_plus', '1.50')); $weight_primary_category = floatval(get_option('aieo_chat_rerank_primary_category', '1.20')); $weight_needs = floatval(get_option('aieo_chat_rerank_needs', '1.00')); $weight_accessories = floatval(get_option('aieo_chat_rerank_accessories', '0.60')); $weight_recency = floatval(get_option('aieo_chat_rerank_recency', '1.30')); $preferred_brands_raw = get_option('aieo_chat_rerank_preferred_brands', ''); $preferred_brands = array(); if (!empty($preferred_brands_raw)) { $brands_array = array_map('trim', explode(',', $preferred_brands_raw)); foreach ($brands_array as $brand) { if (!empty($brand)) { $preferred_brands[] = mb_strtolower($brand, 'UTF-8'); } } } $accessory_keywords = array( 'εργαλεία', 'tools', 'αξεσουάρ', 'accessories', 'μπολ', 'bowl', 'πινέλο', 'brush', 'στίφτης', 'holder', 'θήκη', 'case', 'κλιπ', 'clip', 'καπάκι', 'cap' ); foreach ($results as &$result) { $post = $result['post']; if (!$post || !isset($post->post_type)) { continue; } $original_score = $result['score']; $boost_multiplier = 1.0; $boost_reasons = array(); if ($post->post_type === 'product' || $post->post_type === 'product_variation') { $product = wc_get_product($post->ID); if ($product) { if ($product->is_type('variable')) { $variations = $product->get_children(); $variation_count = count($variations); if ($variation_count > 50 && $weight_variations_50_plus != 1.0) { $boost_multiplier *= $weight_variations_50_plus; $boost_reasons[] = "50+ variations (×{$weight_variations_50_plus})"; } elseif ($variation_count >= 10 && $weight_variations_10_50 != 1.0) { $boost_multiplier *= $weight_variations_10_50; $boost_reasons[] = "10-50 variations (×{$weight_variations_10_50})"; } elseif ($variation_count >= 2 && $weight_variations_2_10 != 1.0) { $boost_multiplier *= $weight_variations_2_10; $boost_reasons[] = "2-10 variations (×{$weight_variations_2_10})"; } } $categories = wp_get_post_terms($post->ID, 'product_cat', array('fields' => 'names')); $is_accessory = false; if (!empty($categories) && !is_wp_error($categories)) { foreach ($categories as $cat_name) { $cat_lower = mb_strtolower($cat_name, 'UTF-8'); foreach ($accessory_keywords as $keyword) { if (strpos($cat_lower, $keyword) !== false) { $is_accessory = true; break 2; } } } } if (!$is_accessory) { $title_lower = mb_strtolower($post->post_title, 'UTF-8'); foreach ($accessory_keywords as $keyword) { if (strpos($title_lower, $keyword) !== false) { $main_product_terms = array('βαφή', 'color', 'κρέμα', 'cream', 'λάδι', 'oil', 'σαμπουάν', 'shampoo'); $has_main_term = false; foreach ($main_product_terms as $main_term) { if (strpos($title_lower, $main_term) !== false) { $has_main_term = true; break; } } if (!$has_main_term) { $is_accessory = true; break; } } } } if ($is_accessory && $weight_accessories != 1.0) { $boost_multiplier *= $weight_accessories; $boost_reasons[] = "accessory penalty (×{$weight_accessories})"; } if (!$is_accessory && !empty($categories) && $weight_primary_category != 1.0) { $primary_categories = array( 'βαφές μαλλιών', 'hair color', 'επαγγελματικές βαφές', 'ημιμόνιμα βερνίκια', 'gel polish', 'σαμπουάν', 'shampoo', 'conditioner', 'μαλακτικά', 'κρέμες προσώπου', 'face cream', 'serum' ); foreach ($categories as $cat_name) { $cat_lower = mb_strtolower($cat_name, 'UTF-8'); foreach ($primary_categories as $primary_cat) { if (strpos($cat_lower, $primary_cat) !== false) { $boost_multiplier *= $weight_primary_category; $boost_reasons[] = "primary category (×{$weight_primary_category})"; break 2; } } } } if (!empty($preferred_brands)) { $product_brands = wp_get_post_terms($post->ID, 'pa_brand', array('fields' => 'names')); if (empty($product_brands) || is_wp_error($product_brands)) { $brand_attr = $product->get_attribute('brand'); if (!empty($brand_attr)) { $product_brands = array($brand_attr); } } $brand_matched = false; if (!empty($product_brands) && !is_wp_error($product_brands)) { foreach ($product_brands as $brand_name) { $brand_lower = mb_strtolower($brand_name, 'UTF-8'); foreach ($preferred_brands as $preferred) { if (strpos($brand_lower, $preferred) !== false || strpos($preferred, $brand_lower) !== false) { $boost_multiplier *= 1.3; $boost_reasons[] = "preferred brand ({$brand_name})"; $brand_matched = true; break 2; } } } } if (!$brand_matched && in_array((string)$post->ID, $preferred_brands)) { $boost_multiplier *= 1.3; $boost_reasons[] = "preferred product ID"; } } if ($weight_recency != 1.0 && class_exists('AIEO_DMM_Brand_Discounts')) { $newness_id = $product->is_type('variation') ? $product->get_parent_id() : $post->ID; if (AIEO_DMM_Brand_Discounts::is_new_product($newness_id)) { $boost_multiplier *= $weight_recency; $boost_reasons[] = "new product (×{$weight_recency})"; } } } } $result['original_score'] = $original_score; $result['score'] = min(1.0, $original_score * $boost_multiplier); $result['boost_multiplier'] = $boost_multiplier; $result['boost_reasons'] = $boost_reasons; } usort($results, function($a, $b) { return $b['score'] <=> $a['score']; }); return $results; } private function select_recommendation_cards($assistant_message, $filtered_results) { if (empty($filtered_results)) { return array(); } $by_id = array(); $by_url = array(); foreach ($filtered_results as $r) { $pid = isset($r['post_id']) ? (int) $r['post_id'] : 0; if ($pid <= 0) { continue; } $by_id[$pid] = $r; $perma = get_permalink($pid); if ($perma) { $by_url[$this->normalize_url($perma)] = $pid; } } if (empty($by_id)) { return array(); } $linked = array(); if (preg_match_all('/\]\(\s*(https?:\/\/[^)\s]+?)\s*\)/i', (string) $assistant_message, $mm)) { foreach ($mm[1] as $raw_url) { $u = $this->normalize_url($raw_url); $pid = isset($by_url[$u]) ? $by_url[$u] : 0; if (!$pid) { $resolved = (int) url_to_postid($raw_url); if ($resolved > 0 && isset($by_id[$resolved])) { $pid = $resolved; } } if ($pid > 0 && isset($by_id[$pid]) && !in_array($pid, $linked, true)) { $linked[] = $pid; } } } if (!empty($linked)) { return apply_filters('aieo_chat_recommendation_post_ids', $linked, $assistant_message, $filtered_results); } $floor = (float) get_option('aieo_chat_card_min_score', 55) / 100; $fallback = array(); foreach ($filtered_results as $r) { $pid = isset($r['post_id']) ? (int) $r['post_id'] : 0; if ($pid <= 0) { continue; } $post = get_post($pid); if (!$post) { continue; } $is_product = in_array($post->post_type, array('product', 'product_variation'), true) || (class_exists('AIEO_Content_Types') && AIEO_Content_Types::is_learning_post_type($post->post_type)); $score = isset($r['score']) ? (float) $r['score'] : 0.0; if ($is_product && $score >= $floor) { $fallback[] = $pid; } } return apply_filters('aieo_chat_recommendation_post_ids', $fallback, $assistant_message, $filtered_results); } private function normalize_url($url) { $url = trim(html_entity_decode((string) $url)); $url = preg_replace('#^https?://#i', '', $url); return rtrim($url, '/'); } private function filter_results_by_quotas($results) { if (empty($results)) { return array(); } $results = $this->rerank_with_business_logic($results); $min_products = (int) get_option('aieo_chat_min_products', 3); $max_products = (int) get_option('aieo_chat_max_products', 10); $product_threshold = (float) get_option('aieo_chat_product_threshold', 50) / 100; $min_posts = (int) get_option('aieo_chat_min_posts', 2); $max_posts = (int) get_option('aieo_chat_max_posts', 5); $post_threshold = (float) get_option('aieo_chat_post_threshold', 50) / 100; $elearning = 'elearning' === $this->detect_site_type(); $learning_types = $elearning && class_exists('AIEO_Content_Types') ? AIEO_Content_Types::learning_post_types() : array(); if ($elearning) { $product_ids = array(); foreach ($results as $r) { $p = isset($r['post_id']) ? get_post((int) $r['post_id']) : null; if ($p && in_array($p->post_type, array('product', 'product_variation'), true)) { $product_ids[] = (int) ('product_variation' === $p->post_type && $p->post_parent ? $p->post_parent : $p->ID); } } $access = empty($product_ids) ? array() : (array) apply_filters('rooster/learning/access_for_products', array(), array_values(array_unique($product_ids))); if (!empty($access)) { $present = array(); foreach ($results as $r) { $present[(int) $r['post_id']] = true; } $folded = array(); foreach ($results as $r) { $p = isset($r['post_id']) ? get_post((int) $r['post_id']) : null; $key = $p && 'product_variation' === $p->post_type && $p->post_parent ? (int) $p->post_parent : (int) $r['post_id']; $cid = isset($access[$key]) ? (int) $access[$key] : 0; if ($cid > 0) { if (!empty($present[$cid])) { continue; } $r['post_id'] = $cid; $r['post'] = get_post($cid); $present[$cid] = true; } $folded[] = $r; } $results = $folded; } } $all_learning = class_exists('AIEO_Content_Types') ? AIEO_Content_Types::learning_post_types() : array(); if (!empty($all_learning)) { $check = array(); foreach ($results as $r) { $p = isset($r['post_id']) ? get_post((int) $r['post_id']) : null; if ($p && in_array($p->post_type, $all_learning, true)) { $check[] = (int) $p->ID; } } if (!empty($check)) { $ok = array_map('intval', (array) apply_filters('rooster/learning/indexable_posts', $check)); $kept = array(); foreach ($results as $r) { $pid = (int) ($r['post_id'] ?? 0); if (in_array($pid, $check, true) && !in_array($pid, $ok, true)) { continue; } $kept[] = $r; } $results = $kept; } } $products = array(); $posts_pages = array(); $vitals_helper = AI_eShop_Chat_Product_Vitals_Helper::get_instance(); $use_vitals_stock = $vitals_helper->is_vitals_table_available(); foreach ($results as $result) { $post = get_post($result['post_id']); if (!$post) { continue; } if ('publish' !== $post->post_status || '' !== (string) $post->post_password) { continue; } if (!empty($learning_types) && in_array($post->post_type, $learning_types, true)) { if ($result['score'] >= $product_threshold) { $products[] = $result; } } elseif ($elearning && ($post->post_type === 'product' || $post->post_type === 'product_variation')) { if ($result['score'] >= $post_threshold) { $posts_pages[] = $result; } } elseif ($post->post_type === 'product' || $post->post_type === 'product_variation') { if ($result['score'] >= $product_threshold) { $is_in_stock = true; if ($use_vitals_stock) { $vitals = $vitals_helper->get_product_vitals($result['post_id']); if ($vitals) { $is_in_stock = isset($vitals->stock) && $vitals->stock > 0; } } elseif (function_exists('wc_get_product')) { $product = wc_get_product($result['post_id']); if ($product) { $is_in_stock = $product->is_in_stock(); } } if ($is_in_stock) { $products[] = $result; } } } else { if ($result['score'] >= $post_threshold) { $posts_pages[] = $result; } } } $selected_products = array_slice($products, 0, $max_products); $selected_posts = array_slice($posts_pages, 0, $max_posts); $filtered_results = array(); $product_index = 0; $post_index = 0; $product_count = count($selected_products); $post_count = count($selected_posts); while ($product_index < $product_count || $post_index < $post_count) { if ($product_index < $product_count) { $filtered_results[] = $selected_products[$product_index]; $product_index++; } if ($post_index < $post_count) { $filtered_results[] = $selected_posts[$post_index]; $post_index++; } } return $filtered_results; } private function build_context_from_results($results) { if (empty($results)) { return ''; } $context_parts = array(); $context_parts[] = "Here is the most relevant content from our store:\n"; $learning = $this->learning_details($results); foreach ($results as $index => $result) { $post_data = AI_eShop_Chat_Content_Extractor::get_post_data($result['post_id']); if (!$post_data) { continue; } if (isset($learning['types']) && in_array($post_data['type'], $learning['types'], true)) { $pid = (int) $post_data['id']; $item_text = sprintf( "%d. %s [%s] (%.2f%% match)\n %s\n URL: %s", $index + 1, $post_data['title'], isset($post_data['kind']) ? $post_data['kind'] : ucfirst($post_data['type']), $result['score'] * 100, $post_data['content'], $post_data['url'] ); if (!empty($post_data['categories'])) { $item_text .= "\n Categories: " . implode(', ', $post_data['categories']); } foreach ((array) ($learning['facts'][$pid] ?? array()) as $label => $value) { if ('' !== trim((string) $value)) { $item_text .= "\n " . (string) $label . ': ' . trim((string) $value); } } $course_id = (int) ($learning['course_of'][$pid] ?? 0); if ($course_id > 0 && $course_id !== $pid) { $item_text .= "\n Part of the course: " . get_the_title($course_id) . ' — ' . get_permalink($course_id); } $lessons = array_values(array_filter(array_map('strval', (array) ($learning['outline'][$pid] ?? array())), 'strlen')); if (!empty($lessons)) { $item_text .= "\n Covers: " . implode(', ', array_slice($lessons, 0, 8)) . (count($lessons) > 8 ? sprintf(' (+%d more)', count($lessons) - 8) : ''); } $enrol_id = $course_id > 0 ? $course_id : $pid; if (!empty($learning['enrol'][$enrol_id]['text'])) { $item_text .= "\n " . (string) $learning['enrol'][$enrol_id]['text']; if (!empty($learning['enrol'][$enrol_id]['url'])) { $item_text .= ' — ' . (string) $learning['enrol'][$enrol_id]['url']; } } } elseif ($post_data['type'] === 'product') { $item_text = sprintf( "%d. %s (%.2f%% match)\n Price: %s\n %s\n URL: %s", $index + 1, $post_data['title'], $result['score'] * 100, $post_data['price_html'], $post_data['short_description'], $post_data['url'] ); if (!empty($post_data['categories'])) { $item_text .= "\n Categories: " . implode(', ', $post_data['categories']); } if ($post_data['rating'] > 0) { $item_text .= sprintf( "\n Rating: %.1f/5 (%d reviews)", $post_data['rating'], $post_data['review_count'] ); } if (!$post_data['in_stock']) { $item_text .= "\n Status: OUT OF STOCK"; } elseif ($post_data['on_sale']) { $item_text .= "\n Status: ON SALE"; } } else { $item_text = sprintf( "%d. %s [%s] (%.2f%% match)\n %s\n URL: %s", $index + 1, $post_data['title'], isset($post_data['kind']) ? $post_data['kind'] : ucfirst($post_data['type']), $result['score'] * 100, $post_data['content'], $post_data['url'] ); if (!empty($post_data['categories'])) { $item_text .= "\n Categories: " . implode(', ', $post_data['categories']); } if (!empty($post_data['date'])) { $item_text .= "\n Published: " . $post_data['date']; } } $context_parts[] = $item_text; } return implode("\n\n", $context_parts); } private function learning_details($results) { $types = class_exists('AIEO_Content_Types') ? AIEO_Content_Types::learning_post_types() : array(); $out = array('types' => $types, 'facts' => array(), 'course_of' => array(), 'outline' => array(), 'enrol' => array()); if (empty($types)) { return $out; } $ids = array(); $courses = array(); $lessons = array(); foreach ((array) $results as $r) { $post = isset($r['post_id']) ? get_post((int) $r['post_id']) : null; if (!$post || !in_array($post->post_type, $types, true)) { continue; } $ids[] = (int) $post->ID; if ('rooster_course' === $post->post_type) { $courses[] = (int) $post->ID; } else { $lessons[] = (int) $post->ID; } } if (empty($ids)) { return $out; } $out['facts'] = (array) apply_filters('rooster/learning/facts_for_posts', array(), $ids); if (!empty($lessons)) { $out['course_of'] = (array) apply_filters('rooster/learning/course_of_posts', array(), $lessons); foreach ($out['course_of'] as $cid) { if ((int) $cid > 0) { $courses[] = (int) $cid; } } } $courses = array_values(array_unique($courses)); if (!empty($courses)) { $out['outline'] = (array) apply_filters('rooster/learning/outline_of_courses', array(), $courses); $out['enrol'] = (array) apply_filters('rooster/learning/enrol_for_courses', array(), $courses); } return $out; } private function detect_site_type() { $enabled_types = AIEO_Content_Types::chat_post_types(); $learning = AIEO_Content_Types::learning_post_types(); if (!empty($learning) && array() !== array_intersect($learning, $enabled_types) && self::course_count() > 0) { return 'elearning'; } $is_woocommerce = class_exists('WooCommerce') && in_array('product', $enabled_types); $has_posts = in_array('post', $enabled_types); $has_pages = in_array('page', $enabled_types); $has_products = in_array('product', $enabled_types); if ($is_woocommerce && $has_products) { return 'eshop'; } elseif ($has_posts && !$has_products) { return 'blog'; } elseif ($has_pages && !$has_posts && !$has_products) { return 'portal'; } else { return 'general'; } } private static function course_count() { if (class_exists('AIEO_SEO_Score') && method_exists('AIEO_SEO_Score', 'course_count')) { return (int) AIEO_SEO_Score::course_count(); } if (!post_type_exists('rooster_course')) { return 0; } $counts = wp_count_posts('rooster_course'); return isset($counts->publish) ? (int) $counts->publish : 0; } private function build_system_prompt($context) { $bot_name = get_option('aieo_chat_bot_name', 'Rooster One'); $site_name = get_bloginfo('name'); $site_tagline = get_bloginfo('description'); $site_type = $this->detect_site_type(); $site_is = get_option('aieo_chat_site_identity_is', ''); $site_is_not = get_option('aieo_chat_site_identity_is_not', ''); switch ($site_type) { case 'eshop': $role_description = "a helpful shopping assistant for {$site_name}, an online store"; $purpose = "help customers find the perfect products based on their needs and questions"; $content_label = "Products"; $not_found_message = "I don't have that product in stock, but here are similar items we have"; $empty_message = "I don't have any products matching your request right now"; break; case 'elearning': $role_description = "a helpful learning assistant for {$site_name}, an online learning site"; $purpose = "help learners find the right courses and lessons for what they want to learn, say what each covers, and point them to where they can enrol"; $content_label = "Courses and lessons"; $not_found_message = "I don't have a course on that exact topic, but here are related courses and lessons"; $empty_message = "I don't have any courses or lessons matching your request right now"; break; case 'blog': $role_description = "a helpful assistant for {$site_name}"; $purpose = "help visitors find relevant articles and information based on their questions"; $content_label = "Articles"; $not_found_message = "I don't have an article about that specific topic, but here are some related articles"; $empty_message = "I don't have any articles matching your request right now"; break; case 'portal': $role_description = "a helpful assistant for {$site_name}"; $purpose = "help visitors find relevant information and resources based on their questions"; $content_label = "Resources"; $not_found_message = "I don't have information about that specific topic, but here are some related resources"; $empty_message = "I don't have any resources matching your request right now"; break; default: $role_description = "a helpful assistant for {$site_name}"; $purpose = "help visitors find relevant content and information based on their questions"; $content_label = "Content"; $not_found_message = "I don't have that specific content, but here are some related items"; $empty_message = "I don't have any content matching your request right now"; break; } $prompt = "You are {$bot_name}, {$role_description}.\n\n"; if (!empty($site_tagline)) { $prompt .= "Site tagline: \"{$site_tagline}\"\n\n"; } if (!empty($site_is) || !empty($site_is_not)) { $prompt .= "SITE IDENTITY (Important for accurate responses):\n"; if (!empty($site_is)) { $prompt .= "What this site IS: {$site_is}\n"; } if (!empty($site_is_not)) { $prompt .= "What this site is NOT: {$site_is_not}\n"; $prompt .= "IMPORTANT: Never refer to this site or business using terms from the 'is NOT' list above.\n"; } $prompt .= "\n"; } $prompt .= "Your role is to {$purpose}.\n\n"; $prompt .= "CRITICAL RULES - YOU MUST FOLLOW THESE EXACTLY:\n"; $prompt .= "1. ONLY recommend items from the 'Available {$content_label}' list below - DO NOT make up or suggest items not in this list\n"; $prompt .= "2. COPY the EXACT URL from the list - NEVER create, modify, or guess URLs\n"; $prompt .= "3. Every item mention MUST be a markdown link using the EXACT URL from the list\n"; $prompt .= "4. If asked about items not in the list, say: '{$not_found_message}'\n"; $prompt .= "5. If the list is empty or has no suitable items, say: '{$empty_message}'\n\n"; $prompt .= "URL FORMAT EXAMPLE:\n"; $prompt .= "If the list shows:\n"; $prompt .= " 1. Example Item Title (70% match)\n"; $prompt .= " URL: http://example.com/item/example-title\n"; $prompt .= "Then you MUST write: [Example Item Title](http://example.com/item/example-title)\n"; $prompt .= "Copy the URL EXACTLY - do not change anything!\n\n"; $prompt .= "Guidelines:\n"; $prompt .= "- Be friendly, helpful, and concise\n"; if ($site_type === 'eshop') { $prompt .= "- Mention prices and key features when relevant\n"; $prompt .= "- If products are out of stock, mention it clearly\n"; $prompt .= "- If products are on sale, highlight the savings\n"; } if ($site_type === 'elearning') { $prompt .= "- Recommend courses first; a lesson belongs to its course, so name the course it is part of when you mention a lesson\n"; $prompt .= "- Mention the category, level, duration, format or what the learner will be able to do when the list says so\n"; $prompt .= "- An 'Enrol:' line says how access is obtained (free, a price, members only, coming soon): say it as the way to enrol, never as a product in its own right\n"; $prompt .= "- Never recommend a shop product on its own: if something is for sale, it is the way to enrol in a course, so recommend the course\n"; } $prompt .= "- Use **bold** for emphasis\n"; $prompt .= "- Keep responses concise (roughly under 200 words) — but when the visitor asks for a list (e.g. all our physical store locations), include EVERY relevant item with its details even if that runs longer\n\n"; $learner_id = get_current_user_id(); if ($learner_id > 0) { $learner = trim((string) apply_filters('rooster/learning/learner_context', '', $learner_id)); if ('' !== $learner) { $prompt .= "THIS LEARNER (signed in; use it to personalise and to suggest what comes next, never repeat it back verbatim):\n"; $prompt .= mb_substr($learner, 0, 1200) . "\n\n"; } } $business_info_context = $this->get_business_info_context(); if (!empty($business_info_context)) { $prompt .= "BUSINESS INFORMATION (Use this for store policies, shipping, locations, FAQs):\n\n"; $prompt .= $business_info_context; $prompt .= "\n\n"; } $store_locations_context = $this->get_store_locations_context(); if (!empty($store_locations_context)) { $prompt .= "PHYSICAL STORES (authoritative — ALWAYS use this list for any question about our physical shops / locations / addresses; the URLs here may be linked):\n\n"; $prompt .= $store_locations_context; $prompt .= "\n\n"; } if (!empty($context)) { $prompt .= "Available {$content_label}:\n\n"; $prompt .= $context; } else { $prompt .= "Note: No specific items match the visitor's query. Provide general assistance."; } $prompt .= $this->language_directive(); return $prompt; } private function language_directive() { $locale = function_exists('determine_locale') ? determine_locale() : get_locale(); $locale = (string) apply_filters('aieo_chat_locale', $locale); $name = $this->language_name($locale); if ($name === '') { return ''; } return " LANGUAGE " . "Write your reply in {$name}. That is the language of this shop and its customers. " . "A product name, brand or SKU is NOT a language signal - brands are spelled the same " . "in every language, so a message containing only one (for example a brand on its own) " . "is still a {$name} question and must be answered in {$name}. " . "The single exception: if the customer writes a full sentence in another language, " . "reply in that language instead."; } private function language_name($locale) { $code = strtolower(substr((string) $locale, 0, 2)); $map = array( 'el' => 'Greek', 'en' => 'English', 'de' => 'German', 'fr' => 'French', 'es' => 'Spanish', 'it' => 'Italian', 'pt' => 'Portuguese', 'nl' => 'Dutch', 'pl' => 'Polish', 'ro' => 'Romanian', 'bg' => 'Bulgarian', 'tr' => 'Turkish', 'ru' => 'Russian', 'sv' => 'Swedish', 'da' => 'Danish', 'fi' => 'Finnish', 'cs' => 'Czech', 'hu' => 'Hungarian', 'sk' => 'Slovak', 'hr' => 'Croatian', 'sr' => 'Serbian', 'ar' => 'Arabic', 'he' => 'Hebrew', 'zh' => 'Chinese', 'ja' => 'Japanese', 'ko' => 'Korean', ); $name = isset($map[$code]) ? $map[$code] : ''; return (string) apply_filters('aieo_chat_language_name', $name, $locale); } private function get_business_info_context() { $business_info_manager = AI_eShop_Chat_Business_Info_Manager::get_instance(); return $business_info_manager->get_context_for_chat(); } private function get_store_locations_context() { if (!class_exists('AIEO_Google_Reviews_Locations')) { return ''; } $locs = AIEO_Google_Reviews_Locations::list_all(true); if (empty($locs)) { return ''; } $url_map = array(); $store_posts = get_posts(array( 'post_type' => array('aieo_store', 'store_location'), 'post_status' => 'publish', 'numberposts' => -1, 'fields' => 'ids', 'suppress_filters' => true, )); foreach ($store_posts as $pid) { $lid = (int) get_post_meta($pid, '_aieo_grv_location_id', true); if ($lid > 0) { $url_map[$lid] = get_permalink($pid); } } $out = "## " . __('Physical Store Locations', 'ai-eshop-optimizer') . "\n"; $out .= __('These are ALL of our physical stores. When the visitor asks where our shops/stores are (or about a specific area), list EVERY relevant store below with its address — never omit any.', 'ai-eshop-optimizer') . "\n\n"; foreach ($locs as $l) { $label = isset($l['label']) ? trim((string) $l['label']) : ''; if ('' === $label) { continue; } $out .= "### {$label}\n"; $addr = array(); foreach (array('street', 'locality', 'postcode', 'region') as $k) { if (!empty($l[$k])) { $addr[] = (string) $l[$k]; } } if ($addr) { $out .= "Address: " . implode(', ', $addr) . "\n"; } if (!empty($l['phone'])) { $out .= "Phone: {$l['phone']}\n"; } $url = ''; if (isset($l['id']) && isset($url_map[(int) $l['id']])) { $url = $url_map[(int) $l['id']]; } elseif (!empty($l['gbp_url'])) { $url = (string) $l['gbp_url']; } if ('' !== $url) { $out .= "Page: {$url}\n"; } $out .= "\n"; } return $out; } private function process_simple_chat($message, $conversation_id) { $bot_name = get_option('aieo_chat_bot_name', 'Rooster One'); $store_name = get_bloginfo('name'); $system_prompt = "You are {$bot_name}, a helpful assistant for {$store_name}. "; $system_prompt .= "Provide friendly and helpful responses to customer questions."; $messages = array( array( 'role' => 'user', 'content' => $message ) ); $api = AI_eShop_Chat_Anthropic_API::get_instance(); $response = $api->chat_completion($messages, array( 'system' => $system_prompt, 'source' => AIEO_AI_Sources::CHAT_CLIENT )); if (is_wp_error($response)) { return $response; } $assistant_message = isset($response['content'][0]['text']) ? $response['content'][0]['text'] : ''; $db = AI_eShop_Chat_Database_Manager::get_instance(); $db->save_chat_message($conversation_id, $message, $assistant_message); return array( 'message' => $assistant_message, 'products' => array(), 'conversation_id' => $conversation_id ); } public function handle_chat_message() { check_ajax_referer('wp_rest', 'nonce'); $message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash( $_POST['message'] )) : ''; $conversation_id = isset($_POST['conversation_id']) ? sanitize_text_field(wp_unslash( $_POST['conversation_id'] )) : ''; if (empty($message)) { wp_send_json_error(array('message' => __('Message cannot be empty', 'ai-eshop-optimizer'))); } if (empty($conversation_id)) { $conversation_id = wp_generate_uuid4(); } $result = $this->process_chat_message($message, $conversation_id); if (is_wp_error($result)) { wp_send_json_error(array( 'message' => $result->get_error_message(), 'code' => $result->get_error_code() )); } wp_send_json_success($result); } public function get_chat_config() { return array( 'enabled' => get_option('aieo_chat_enabled', '0') === '1', 'bot_name' => get_option('aieo_chat_bot_name', 'Rooster One'), 'welcome_message' => self::chat_copy('aieo_chat_welcome_message', __('Hello! How can I help you find the perfect product today?', 'ai-eshop-optimizer')), 'inputPlaceholder' => self::chat_copy('aieo_chat_input_placeholder', __('Ask about products...', 'ai-eshop-optimizer')), 'max_results' => (int) get_option('aieo_chat_max_results', 10), 'hide_images' => get_option('aieo_chat_hide_images', '0') === '1', 'require_login' => get_option('aieo_chat_require_login', '0') === '1', 'show_terms' => get_option('aieo_chat_show_terms', '0') === '1', 'category_filter_enabled' => get_option('aieo_chat_category_filter_enabled', '0') === '1', 'is_logged_in' => is_user_logged_in() ); } }