false, 'show_more_url' => '', ]; public function permission_check( WP_REST_Request $request ) { unset( $request ); // Public storefront endpoint. Request safety is enforced in handle() // via input sanitization, rate limiting, and model usage limits. return __return_true(); } public function handle( WP_REST_Request $request ) { $debug_id = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'acosmaia_', true ); $this->last_product_meta = [ 'has_more' => false, 'show_more_url' => '', ]; if ( ! class_exists( 'WooCommerce' ) ) { $this->report_error_event( $debug_id, 'woocommerce_missing', 'WooCommerce is required.' ); return new WP_REST_Response( [ 'error' => 'WooCommerce is required.' ], 500 ); } if ( ! $this->allow_request_rate() ) { return new WP_REST_Response( [ 'error' => 'Too many requests. Please wait a minute and try again.' ], 429 ); } $message = sanitize_text_field( (string) $request->get_param( 'message' ) ); if ( $message === '' ) { return new WP_REST_Response( [ 'error' => 'Message is required.' ], 400 ); } $message_lower = strtolower( $message ); if ( $this->is_order_details_request( $message_lower ) ) { $order_details_message = $this->build_current_user_order_details_message(); $this->increment_counter( 'acosmaia_total_chats' ); return new WP_REST_Response( [ 'message' => $order_details_message, 'warning' => '', 'products' => [], 'show_more_url' => '', 'show_more_label' => '', 'applied_filters' => [], 'total_found' => 0, 'debug_id' => $debug_id, 'usage' => [ 'total_tokens' => 0 ], ], 200 ); } $access_warning = $this->get_blocking_access_warning( $message_lower ); if ( '' !== $access_warning ) { $this->increment_counter( 'acosmaia_total_chats' ); return new WP_REST_Response( [ 'message' => $access_warning, 'warning' => $access_warning, 'products' => [], 'show_more_url' => '', 'show_more_label' => '', 'applied_filters' => [], 'total_found' => 0, 'debug_id' => $debug_id, 'usage' => [ 'total_tokens' => 0 ], ], 200 ); } $page_access_warning = $this->get_page_access_warning( $message_lower ); // Check if API is configured $ai_client = ACOSMAIA_AI_API_Client::from_settings(); $is_api_configured = $ai_client->is_configured(); if ( ! $this->usage_limit_available() && $is_api_configured ) { $limit_message = $this->get_token_finished_message(); return new WP_REST_Response( [ 'message' => $limit_message, 'warning' => $limit_message, 'error' => $limit_message, ], 429 ); } $query_plan = []; if ( $is_api_configured && $this->should_plan_product_query( $message_lower ) ) { $query_plan = $this->generate_product_query_plan( '', $message ); } $finder_result = $this->get_product_finder()->find( $message, $query_plan ); if ( ! is_array( $finder_result ) ) { $this->increment_counter( 'acosmaia_failed_queries' ); $this->report_error_event( $debug_id, 'product_finder_invalid_result', 'Product finder returned an invalid result.' ); return new WP_REST_Response( [ 'message' => get_option( 'acosmaia_fallback_response', 'Sorry, I am unable to answer right now. Please contact support.' ), 'warning' => $page_access_warning, 'products' => [], 'show_more_url' => '', 'show_more_label' => '', 'applied_filters' => [], 'total_found' => 0, 'debug_id' => $debug_id, 'error' => 'Invalid product finder response.', ], 200 ); } $products = isset( $finder_result['products'] ) && is_array( $finder_result['products'] ) ? $finder_result['products'] : []; $product_cards = $this->build_product_cards( $products ); $show_more_url = isset( $finder_result['show_more_url'] ) ? (string) $finder_result['show_more_url'] : ''; $show_more_label = $show_more_url ? 'Show more products' : ''; $total_found = isset( $finder_result['total_found'] ) ? (int) $finder_result['total_found'] : count( $products ); $applied_filters = isset( $finder_result['applied_filters'] ) && is_array( $finder_result['applied_filters'] ) ? $finder_result['applied_filters'] : []; $this->increment_counter( 'acosmaia_total_chats' ); if ( ! $is_api_configured ) { return new WP_REST_Response( [ 'message' => $this->fallback_message( $products, $message, true ), 'warning' => $page_access_warning, 'products' => $product_cards, 'show_more_url' => $show_more_url, 'show_more_label' => $show_more_label, 'applied_filters' => $applied_filters, 'total_found' => $total_found, 'debug_id' => $debug_id, 'usage' => [ 'total_tokens' => 0 ], ], 200 ); } $system_prompt = $this->build_system_prompt( $products, $message ); if ( ! $ai_client->is_configured() ) { return new WP_REST_Response( [ 'message' => $this->fallback_message( $products, $message, true ), 'warning' => $page_access_warning, 'products' => $product_cards, 'show_more_url' => $show_more_url, 'show_more_label' => $show_more_label, 'applied_filters' => $applied_filters, 'total_found' => $total_found, 'debug_id' => $debug_id, 'usage' => [ 'total_tokens' => 0 ], ], 200 ); } $request_response = $ai_client->request( $system_prompt, $message ); if ( is_wp_error( $request_response ) ) { $this->increment_counter( 'acosmaia_failed_queries' ); $error_message = (string) $request_response->get_error_message(); $this->report_error_event( $debug_id, 'ai_request_error', $error_message ); $is_token_limit = $this->is_token_limit_error_message( $error_message ); $assistant_message = $is_token_limit ? $this->get_token_finished_message() : get_option( 'acosmaia_fallback_response', 'Sorry, I am unable to answer right now. Please contact support.' ); return new WP_REST_Response( [ 'message' => $assistant_message, 'warning' => $is_token_limit ? $assistant_message : $page_access_warning, 'products' => $product_cards, 'show_more_url' => $show_more_url, 'show_more_label' => $show_more_label, 'applied_filters' => $applied_filters, 'total_found' => $total_found, 'debug_id' => $debug_id, 'error' => $error_message, ], 200 ); } $parsed = $ai_client->parse_response( $request_response ); if ( isset( $parsed['error'] ) ) { $this->increment_counter( 'acosmaia_failed_queries' ); $error_message = (string) $parsed['error']; $this->report_error_event( $debug_id, 'ai_parse_error', $error_message ); $is_token_limit = $this->is_token_limit_error_message( $error_message ); $assistant_message = $is_token_limit ? $this->get_token_finished_message() : get_option( 'acosmaia_fallback_response', 'Sorry, I am unable to answer right now. Please contact support.' ); return new WP_REST_Response( [ 'message' => $assistant_message, 'warning' => $is_token_limit ? $assistant_message : $page_access_warning, 'products' => $product_cards, 'show_more_url' => $show_more_url, 'show_more_label' => $show_more_label, 'applied_filters' => $applied_filters, 'total_found' => $total_found, 'debug_id' => $debug_id, 'error' => $error_message, ], 200 ); } $ai_content = isset( $parsed['content'] ) ? $parsed['content'] : ''; $ai_usage = isset( $parsed['usage'] ) ? $parsed['usage'] : []; $assistant_message = trim( wp_strip_all_tags( (string) $ai_content ) ); if ( ! empty( $product_cards ) ) { $assistant_message = 'Here are some products that match your search.'; } if ( empty( $ai_content ) ) { $this->increment_counter( 'acosmaia_failed_queries' ); $provider_error = 'Unexpected AI response: empty content.'; $this->report_error_event( $debug_id, 'ai_empty_content', $provider_error ); $is_token_limit = $this->is_token_limit_error_message( $provider_error ); $assistant_message = $is_token_limit ? $this->get_token_finished_message() : get_option( 'acosmaia_fallback_response', 'Sorry, I am unable to answer right now. Please contact support.' ); return new WP_REST_Response( [ 'message' => $assistant_message, 'warning' => $is_token_limit ? $assistant_message : $page_access_warning, 'products' => $product_cards, 'show_more_url' => $show_more_url, 'show_more_label' => $show_more_label, 'applied_filters' => $applied_filters, 'total_found' => $total_found, 'debug_id' => $debug_id, 'error' => $provider_error, ], 200 ); } $this->track_usage( $ai_usage ); return new WP_REST_Response( [ 'message' => $assistant_message, 'warning' => $page_access_warning, 'products' => $product_cards, 'show_more_url' => $show_more_url, 'show_more_label' => $show_more_label, 'applied_filters' => $applied_filters, 'total_found' => $total_found, 'debug_id' => $debug_id, 'usage' => [ 'prompt_tokens' => isset( $ai_usage['prompt_tokens'] ) ? (int) $ai_usage['prompt_tokens'] : 0, 'completion_tokens' => isset( $ai_usage['completion_tokens'] ) ? (int) $ai_usage['completion_tokens'] : 0, 'total_tokens' => isset( $ai_usage['total_tokens'] ) ? (int) $ai_usage['total_tokens'] : 0, ], ], 200 ); } private function get_product_finder() { if ( ! $this->product_finder instanceof ACOSMAIA_Product_Finder ) { $this->product_finder = new ACOSMAIA_Product_Finder(); } return $this->product_finder; } private function allow_request_rate() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; $key = 'acosmaia_rate_' . md5( $ip ); $count = (int) get_transient( $key ); if ( $count >= 20 ) { return false; } set_transient( $key, $count + 1, MINUTE_IN_SECONDS ); return true; } private function usage_limit_available() { $daily_limit = (int) get_option( 'acosmaia_daily_limit', 0 ); $monthly_limit = (int) get_option( 'acosmaia_monthly_limit', 0 ); $daily_used = (int) get_option( 'acosmaia_usage_daily_' . current_time( 'Ymd' ), 0 ); $monthly_used = (int) get_option( 'acosmaia_usage_monthly_' . current_time( 'Ym' ), 0 ); if ( $daily_limit > 0 && $daily_used >= $daily_limit ) { return false; } if ( $monthly_limit > 0 && $monthly_used >= $monthly_limit ) { return false; } return true; } private function get_token_finished_message() { return 'Token limit finished for now. Please try again later, or ask the site admin to increase chat token limits/API credits.'; } private function is_token_limit_error_message( $error_message ) { $text = strtolower( trim( (string) $error_message ) ); if ( '' === $text ) { return false; } return strpos( $text, 'quota exceeded' ) !== false || strpos( $text, 'usage limit reached' ) !== false || strpos( $text, 'insufficient credits' ) !== false || strpos( $text, 'out of credits' ) !== false || strpos( $text, 'token limit' ) !== false || strpos( $text, 'billing' ) !== false; } private function track_usage( array $usage ) { $total = isset( $usage['total_tokens'] ) ? (int) $usage['total_tokens'] : 0; if ( $total <= 0 ) { $prompt_tokens = isset( $usage['prompt_tokens'] ) ? (int) $usage['prompt_tokens'] : 0; $completion_tokens = isset( $usage['completion_tokens'] ) ? (int) $usage['completion_tokens'] : 0; $total = $prompt_tokens + $completion_tokens; } if ( $total <= 0 ) { return; } $daily_key = 'acosmaia_usage_daily_' . current_time( 'Ymd' ); $monthly_key = 'acosmaia_usage_monthly_' . current_time( 'Ym' ); update_option( $daily_key, (int) get_option( $daily_key, 0 ) + $total, false ); update_option( $monthly_key, (int) get_option( $monthly_key, 0 ) + $total, false ); } private function increment_counter( $option_key ) { update_option( $option_key, (int) get_option( $option_key, 0 ) + 1, false ); } private function report_error_event( $debug_id, $context, $message ) { $payload = [ 'debug_id' => sanitize_text_field( (string) $debug_id ), 'context' => sanitize_key( (string) $context ), 'message' => sanitize_text_field( (string) $message ), 'timestamp' => current_time( 'mysql' ), ]; // Keep the latest error in a short-lived transient for diagnostics without using runtime logs. set_transient( 'acosmaia_last_error', $payload, 10 * MINUTE_IN_SECONDS ); /** * Allows custom monitoring integrations to capture chat handler errors. * * @param array $payload Error payload with debug_id, context, message and timestamp. */ do_action( 'acosmaia_error_event', $payload ); } private function build_system_prompt( array $products, $message ) { $store_name = get_bloginfo( 'name' ); $assistant = get_option( 'acosmaia_assistant_name', 'AI Assistant' ); $knowledge = get_option( 'acosmaia_knowledge_text', '' ); $restrict = get_option( 'acosmaia_restrict_product_only', 'no' ) === 'yes'; $currency_code = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD'; $currency_symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '$'; $currency_example = wp_strip_all_tags( wc_price( 1234.56 ) ); // Compact product context - only essential info $product_lines = []; foreach ( $products as $product ) { if ( ! $product instanceof WC_Product ) { continue; } // Minimal product format to reduce tokens $price = $product->get_price(); $sale_price = $product->get_sale_price(); $price_text = $sale_price !== '' ? wp_strip_all_tags( wc_price( (float) $sale_price ) ) . ' (was ' . wp_strip_all_tags( wc_price( (float) $price ) ) . ')' : wp_strip_all_tags( wc_price( (float) $price ) ); $product_lines[] = sprintf( '%s - %s - %s', $product->get_name(), $price_text, $product->is_in_stock() ? 'In stock' : 'Out of stock' ); } $product_context = empty( $product_lines ) ? 'No matching products found.' : 'Products: ' . implode( ' | ', array_slice( $product_lines, 0, 3 ) ); $site_context = $this->build_site_context(); $wc_context = $this->build_woocommerce_context(); $policy_context = $this->get_policy_context(); $pages_context = $this->build_enabled_pages_context(); $blog_context = $this->build_blog_page_context( $message ); $knowledge = $this->truncate_context( $knowledge, 1800 ); // Check if enabled pages are available for context $has_enabled_pages = strpos( $pages_context, 'No page access enabled' ) === false && strpos( $pages_context, 'No valid enabled pages found' ) === false; // Build scope section based on restriction and page availability $scope_section = $this->build_scope_section( $restrict, $has_enabled_pages ); return "You are {$assistant} for {$store_name}. You help customers with shopping questions, product information, store policies, and general inquiries.\n\n" . "COMMUNICATION STYLE:\n" . "- Be friendly, professional, and concise\n" . "- Use clear, simple language\n" . "- Ask clarifying questions if the user's request is unclear\n" . "- Format responses with line breaks for readability\n" . "- Use bullet points for lists when relevant\n\n" . "PRODUCT QUESTIONS:\n" . "- Use the provided product information below\n" . "- If multiple products match, highlight 2-3 best options\n" . "- Always include price, availability, and key features\n" . "- When product cards are available in the UI, provide a brief summary and let the cards show details\n" . "- Never invent products, prices, or specifications\n\n" . "SUMMARY REQUESTS:\n" . "- When asked to summarize, provide concise key points (3-5 bullet points max)\n" . "- Focus on the most important information\n" . "- Use clear, easy-to-read formatting\n\n" . "PRICING:\n" . "- Store currency: {$currency_code} ({$currency_symbol})\n" . "- Currency format example: {$currency_example}\n" . "- Always quote prices in {$currency_code} unless user asks for conversion\n\n" . "ORDER & ACCOUNT QUESTIONS:\n" . "- If user mentions orders or account issues, suggest they check My Account or contact support\n" . "- Provide links to relevant pages if available\n\n" . "POLICY QUESTIONS:\n" . "- Answer based on the policies below\n" . "- If policy information is not available, direct user to support\n\n" . "{$scope_section}\n" . "KNOWLEDGE BASE:\n{$knowledge}\n\n" . "STORE INFO:\n{$site_context}\n\n" . "STORE SETUP:\n{$wc_context}\n\n" . "IMPORTANT POLICIES:\n{$policy_context}\n\n" . "BLOG & CONTENT:\n{$blog_context}\n\n" . "AVAILABLE INFORMATION:\n{$pages_context}\n\n" . "AVAILABLE PRODUCTS:\n{$product_context}\n\n" . "CRITICAL RULES:\n" . "- Never make up or invent products, prices, or features\n" . "- If information is not available, say so clearly\n" . "- Be helpful within the scope of available information\n" . "- Keep responses focused and actionable\n" . "- If a question is outside your knowledge, politely explain what information you have access to"; } private function build_scope_section( $restrict, $has_enabled_pages ) { if ( $restrict ) { return "SCOPE:\n" . "- Only answer product and shopping-related questions\n" . "- Do not answer general knowledge or off-topic questions\n" . "- Focus on helping customers find and learn about products\n" . "- Stay on topic and relevant to the store\n"; } if ( $has_enabled_pages ) { return "SCOPE:\n" . "- Help with shopping, products, and store policies\n" . "- Answer questions about the pages and information provided above (these are topics we have content for)\n" . "- For general questions not covered by provided pages, you can provide helpful information if relevant to the store\n" . "- Stay on topic and helpful to customers\n" . "- If a topic is not covered in the provided pages/information, let the user know what you do have access to\n"; } return "SCOPE:\n" . "- Help with shopping, products, and store policies\n" . "- Answer general customer service questions\n" . "- Be helpful and professional in all interactions\n" . "- If information is not available, let the user know and suggest contacting support\n"; } private function build_enabled_pages_context() { $enabled_pages = get_option( 'acosmaia_enabled_pages', [] ); if ( ! is_array( $enabled_pages ) || empty( $enabled_pages ) ) { return 'No page access enabled.'; } $chunks = []; foreach ( array_slice( $enabled_pages, 0, 30 ) as $page_id ) { $page_id = absint( $page_id ); if ( $page_id <= 0 ) { continue; } $page = get_post( $page_id ); if ( ! $page instanceof WP_Post || 'page' !== $page->post_type || 'publish' !== $page->post_status ) { continue; } $title = sanitize_text_field( get_the_title( $page_id ) ); $url = esc_url_raw( get_permalink( $page_id ) ); $content = wp_strip_all_tags( (string) $page->post_content ); $content = preg_replace( '/\s+/', ' ', trim( $content ) ); $content = $this->truncate_context( $content, 3000 ); $chunks[] = "Page: {$title}\nURL: {$url}\nContent: {$content}"; } if ( empty( $chunks ) ) { return 'No valid enabled pages found.'; } return $this->truncate_context( implode( "\n\n", $chunks ), 12000 ); } private function build_blog_page_context( $message = '' ) { $blog_page_id = (int) get_option( 'page_for_posts', 0 ); if ( $blog_page_id <= 0 ) { return 'No blog page configured.'; } $blog_page = get_post( $blog_page_id ); if ( ! $blog_page instanceof WP_Post || 'page' !== $blog_page->post_type || 'publish' !== $blog_page->post_status ) { return 'Blog page exists but is not accessible.'; } $title = sanitize_text_field( get_the_title( $blog_page_id ) ); $url = esc_url_raw( get_permalink( $blog_page_id ) ); // Get blog page content $content = wp_strip_all_tags( (string) $blog_page->post_content ); $content = preg_replace( '/\s+/', ' ', trim( $content ) ); $content = $this->truncate_context( $content, 2000 ); $blog_info = "Blog Page: {$title}\nURL: {$url}"; if ( ! empty( $content ) ) { $blog_info .= "\nContent: {$content}"; } // Check if user is asking about a specific blog post if ( ! empty( $message ) && is_string( $message ) && strlen( $message ) > 3 ) { $specific_posts = $this->find_blog_posts_matching_query( $message ); if ( ! empty( $specific_posts ) ) { $blog_info .= "\n\nMatching Blog Posts:\n"; foreach ( $specific_posts as $post ) { if ( ! $post instanceof WP_Post ) { continue; } $post_title = sanitize_text_field( $post->post_title ); $post_url = esc_url_raw( get_permalink( $post ) ); // Get full post content $post_content = wp_strip_all_tags( $post->post_content ); $post_content = preg_replace( '/\s+/', ' ', trim( $post_content ) ); $post_content = $this->truncate_context( $post_content, 2000 ); $blog_info .= "\nPost: {$post_title}\nURL: {$post_url}\nContent: {$post_content}"; } return $this->truncate_context( $blog_info, 10000 ); } } // Get recent posts from blog with full content $recent_posts = get_posts( [ 'numberposts' => 10, 'post_type' => 'post', 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', ] ); $posts_summary = []; if ( ! empty( $recent_posts ) ) { foreach ( $recent_posts as $post ) { if ( ! $post instanceof WP_Post ) { continue; } $post_title = sanitize_text_field( $post->post_title ); $post_url = esc_url_raw( get_permalink( $post ) ); // Get post excerpt or content $post_content = ''; if ( ! empty( $post->post_excerpt ) ) { $post_content = wp_strip_all_tags( $post->post_excerpt ); } else { $post_content = wp_strip_all_tags( $post->post_content ); } // Clean up whitespace $post_content = preg_replace( '/\s+/', ' ', trim( $post_content ) ); $post_content = $this->truncate_context( $post_content, 800 ); $posts_summary[] = "Post: {$post_title}\nURL: {$post_url}\nContent: {$post_content}"; } } if ( ! empty( $posts_summary ) ) { $blog_info .= "\n\nRecent Posts:\n" . implode( "\n\n", $posts_summary ); } return $this->truncate_context( $blog_info, 8000 ); } private function find_blog_posts_matching_query( $message ) { if ( ! is_string( $message ) || strlen( trim( $message ) ) < 3 ) { return []; } // Search for posts matching keywords from the message $search_terms = $this->extract_search_keywords( strtolower( $message ) ); if ( empty( $search_terms ) ) { return []; } // Try to find exact title matches first $posts = get_posts( [ 'numberposts' => 5, 'post_type' => 'post', 'post_status' => 'publish', 's' => $search_terms, 'orderby' => 'relevance', ] ); // Filter to only include posts that have a reasonable match $matched = []; if ( ! empty( $posts ) ) { $query_lower = strtolower( $message ); foreach ( $posts as $post ) { if ( ! $post instanceof WP_Post ) { continue; } $post_title_lower = strtolower( $post->post_title ); $post_content_lower = strtolower( wp_strip_all_tags( $post->post_content ) ); // Check if post title or content contains significant portion of the query $title_match = 0; $terms = explode( ' ', $search_terms ); foreach ( $terms as $term ) { if ( ! empty( $term ) && strlen( $term ) > 2 ) { if ( strpos( $post_title_lower, $term ) !== false ) { $title_match += 2; } elseif ( strpos( $post_content_lower, $term ) !== false ) { $title_match += 1; } } } if ( $title_match > 0 ) { $matched[] = $post; } } } return $matched; } private function build_site_context() { $cache_key = 'acosmaia_site_context_v2'; $cached = get_transient( $cache_key ); if ( is_string( $cached ) && $cached !== '' ) { return $cached; } $lines = []; $lines[] = 'Site name: ' . sanitize_text_field( get_bloginfo( 'name' ) ); $lines[] = 'Tagline: ' . sanitize_text_field( get_bloginfo( 'description' ) ); $lines[] = 'Site URL: ' . esc_url_raw( home_url( '/' ) ); $lines[] = 'Language: ' . sanitize_text_field( get_bloginfo( 'language' ) ); $lines[] = 'Timezone: ' . sanitize_text_field( (string) wp_timezone_string() ); $lines[] = 'Date format: ' . sanitize_text_field( (string) get_option( 'date_format', 'F j, Y' ) ); $lines[] = 'Time format: ' . sanitize_text_field( (string) get_option( 'time_format', 'g:i a' ) ); $admin_email = get_option( 'admin_email', '' ); if ( is_email( $admin_email ) ) { $lines[] = 'Admin email: ' . sanitize_email( $admin_email ); } if ( function_exists( 'wp_get_theme' ) ) { $theme = wp_get_theme(); if ( $theme ) { $lines[] = 'Theme: ' . sanitize_text_field( (string) $theme->get( 'Name' ) ); } } $important_pages = []; if ( (int) get_option( 'page_on_front', 0 ) > 0 ) { $important_pages[] = [ 'label' => 'Front page', 'id' => (int) get_option( 'page_on_front', 0 ), ]; } if ( (int) get_option( 'page_for_posts', 0 ) > 0 ) { $important_pages[] = [ 'label' => 'Blog page', 'id' => (int) get_option( 'page_for_posts', 0 ), ]; } if ( (int) get_option( 'wp_page_for_privacy_policy', 0 ) > 0 ) { $important_pages[] = [ 'label' => 'Privacy policy', 'id' => (int) get_option( 'wp_page_for_privacy_policy', 0 ), ]; } foreach ( $important_pages as $page_ref ) { $page_id = isset( $page_ref['id'] ) ? (int) $page_ref['id'] : 0; if ( $page_id <= 0 ) { continue; } $title = get_the_title( $page_id ); $url = get_permalink( $page_id ); if ( ! $title || ! $url ) { continue; } $lines[] = sanitize_text_field( (string) $page_ref['label'] ) . ': ' . sanitize_text_field( $title ) . ' (' . esc_url_raw( $url ) . ')'; } $context = $this->truncate_context( implode( "\n", array_filter( $lines ) ), 2400 ); set_transient( $cache_key, $context, 10 * MINUTE_IN_SECONDS ); return $context; } private function build_woocommerce_context() { if ( ! class_exists( 'WooCommerce' ) ) { return 'WooCommerce not installed.'; } $cache_key = 'acosmaia_wc_context_v3'; $cached = get_transient( $cache_key ); if ( is_string( $cached ) && $cached !== '' ) { return $cached; } $lines = []; $currency_code = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD'; $currency_symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '$'; $currency_pos = (string) get_option( 'woocommerce_currency_pos', 'left' ); $lines[] = 'Currency: ' . sanitize_text_field( $currency_code ) . ' (' . sanitize_text_field( $currency_symbol ) . ')'; $lines[] = 'Currency position: ' . sanitize_text_field( $currency_pos ); $lines[] = 'Weight unit: ' . sanitize_text_field( (string) get_option( 'woocommerce_weight_unit', 'kg' ) ); $lines[] = 'Dimension unit: ' . sanitize_text_field( (string) get_option( 'woocommerce_dimension_unit', 'cm' ) ); $lines[] = 'Prices include tax: ' . ( get_option( 'woocommerce_prices_include_tax', 'no' ) === 'yes' ? 'yes' : 'no' ); $lines[] = 'Tax enabled: ' . ( get_option( 'woocommerce_calc_taxes', 'no' ) === 'yes' ? 'yes' : 'no' ); $lines[] = 'Coupons enabled: ' . ( get_option( 'woocommerce_enable_coupons', 'yes' ) === 'yes' ? 'yes' : 'no' ); $lines[] = 'Manage stock: ' . ( get_option( 'woocommerce_manage_stock', 'yes' ) === 'yes' ? 'yes' : 'no' ); $shop_page_url = function_exists( 'wc_get_page_permalink' ) ? wc_get_page_permalink( 'shop' ) : ''; $cart_url = function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : ''; $checkout_url = function_exists( 'wc_get_checkout_url' ) ? wc_get_checkout_url() : ''; $myaccount_url = function_exists( 'wc_get_page_permalink' ) ? wc_get_page_permalink( 'myaccount' ) : ''; if ( $shop_page_url ) { $lines[] = 'Shop URL: ' . esc_url_raw( $shop_page_url ); } if ( $cart_url ) { $lines[] = 'Cart URL: ' . esc_url_raw( $cart_url ); } if ( $checkout_url ) { $lines[] = 'Checkout URL: ' . esc_url_raw( $checkout_url ); } if ( $myaccount_url ) { $lines[] = 'My account URL: ' . esc_url_raw( $myaccount_url ); } $product_counts = wp_count_posts( 'product' ); if ( $product_counts ) { $lines[] = sprintf( 'Product counts: publish=%d draft=%d pending=%d private=%d', isset( $product_counts->publish ) ? (int) $product_counts->publish : 0, isset( $product_counts->draft ) ? (int) $product_counts->draft : 0, isset( $product_counts->pending ) ? (int) $product_counts->pending : 0, isset( $product_counts->private ) ? (int) $product_counts->private : 0 ); } $sale_ids = function_exists( 'wc_get_product_ids_on_sale' ) ? wc_get_product_ids_on_sale() : []; $featured_ids = function_exists( 'wc_get_featured_product_ids' ) ? wc_get_featured_product_ids() : []; $lines[] = 'On-sale products: ' . count( (array) $sale_ids ); $lines[] = 'Featured products: ' . count( (array) $featured_ids ); $recent_products = wc_get_products( [ 'limit' => 20, 'status' => 'publish', 'return' => 'objects', 'orderby' => 'date', 'order' => 'DESC', ] ); $price_values = []; $recent_lines = []; foreach ( $recent_products as $product ) { if ( ! $product instanceof WC_Product ) { continue; } $price = (float) $product->get_price(); if ( $price > 0 ) { $price_values[] = $price; } $recent_lines[] = sprintf( '#%d %s | SKU: %s | Price: %s | Stock: %s | Categories: %s', (int) $product->get_id(), sanitize_text_field( $product->get_name() ), sanitize_text_field( (string) $product->get_sku() ), wp_strip_all_tags( wc_price( $price ) ), $product->is_in_stock() ? 'instock' : 'outofstock', wp_strip_all_tags( wc_get_product_category_list( $product->get_id(), ', ', '', '' ) ) ); } if ( ! empty( $price_values ) ) { $lines[] = 'Recent product price range: min=' . wp_strip_all_tags( wc_price( min( $price_values ) ) ) . ' max=' . wp_strip_all_tags( wc_price( max( $price_values ) ) ); $lines[] = 'Recent product avg price: ' . wp_strip_all_tags( wc_price( array_sum( $price_values ) / max( 1, count( $price_values ) ) ) ); } $categories = get_terms( [ 'taxonomy' => 'product_cat', 'hide_empty' => false, 'number' => 40, 'orderby' => 'count', 'order' => 'DESC', ] ); if ( is_array( $categories ) && ! is_wp_error( $categories ) ) { $cat_lines = []; foreach ( $categories as $cat ) { if ( ! $cat instanceof WP_Term ) { continue; } $cat_lines[] = sanitize_text_field( $cat->name ) . ' (' . (int) $cat->count . ')'; } if ( ! empty( $cat_lines ) ) { $lines[] = 'Product categories: ' . implode( ', ', $cat_lines ); } } $tags = get_terms( [ 'taxonomy' => 'product_tag', 'hide_empty' => false, 'number' => 40, 'orderby' => 'count', 'order' => 'DESC', ] ); if ( is_array( $tags ) && ! is_wp_error( $tags ) ) { $tag_lines = []; foreach ( $tags as $tag ) { if ( ! $tag instanceof WP_Term ) { continue; } $tag_lines[] = sanitize_text_field( $tag->name ) . ' (' . (int) $tag->count . ')'; } if ( ! empty( $tag_lines ) ) { $lines[] = 'Product tags: ' . implode( ', ', $tag_lines ); } } if ( function_exists( 'wc_get_attribute_taxonomies' ) ) { $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( is_array( $attribute_taxonomies ) ) { $attr_lines = []; foreach ( $attribute_taxonomies as $attribute_taxonomy ) { if ( ! isset( $attribute_taxonomy->attribute_name ) ) { continue; } $attr_lines[] = sanitize_text_field( (string) $attribute_taxonomy->attribute_label ) . ' (' . sanitize_text_field( (string) $attribute_taxonomy->attribute_name ) . ')'; } if ( ! empty( $attr_lines ) ) { $lines[] = 'Global attributes: ' . implode( ', ', $attr_lines ); } } } if ( function_exists( 'WC' ) && WC()->payment_gateways() ) { $gateway_lines = []; $available_gateways = WC()->payment_gateways()->payment_gateways(); foreach ( $available_gateways as $gateway ) { if ( ! isset( $gateway->id ) ) { continue; } $gateway_lines[] = sanitize_text_field( (string) $gateway->id ) . ':' . ( ! empty( $gateway->enabled ) && $gateway->enabled === 'yes' ? 'enabled' : 'disabled' ); } if ( ! empty( $gateway_lines ) ) { $lines[] = 'Payment gateways: ' . implode( ', ', $gateway_lines ); } } if ( class_exists( 'WC_Shipping_Zones' ) ) { $shipping_zone_lines = []; $zones = WC_Shipping_Zones::get_zones(); foreach ( $zones as $zone ) { if ( empty( $zone['zone_name'] ) || empty( $zone['shipping_methods'] ) ) { continue; } $method_lines = []; foreach ( $zone['shipping_methods'] as $method ) { if ( ! isset( $method->id ) ) { continue; } $method_lines[] = sanitize_text_field( (string) $method->id ) . ':' . ( ! empty( $method->enabled ) && $method->enabled === 'yes' ? 'enabled' : 'disabled' ); } if ( ! empty( $method_lines ) ) { $shipping_zone_lines[] = sanitize_text_field( (string) $zone['zone_name'] ) . ' [' . implode( ', ', $method_lines ) . ']'; } } if ( ! empty( $shipping_zone_lines ) ) { $lines[] = 'Shipping zones: ' . implode( ' | ', $shipping_zone_lines ); } } if ( ! empty( $recent_lines ) ) { $lines[] = 'Recent products: ' . implode( ' || ', array_slice( $recent_lines, 0, 20 ) ); } $context = $this->truncate_context( implode( "\n", array_filter( $lines ) ), 12000 ); set_transient( $cache_key, $context, 10 * MINUTE_IN_SECONDS ); return $context; } private function truncate_context( $text, $max_chars ) { $text = trim( (string) $text ); $max = max( 200, (int) $max_chars ); if ( strlen( $text ) <= $max ) { return $text; } return substr( $text, 0, $max ) . '...'; } private function get_policy_context() { $pages = [ 'shipping' => wc_get_page_permalink( 'shop' ), 'refund' => function_exists( 'wc_get_account_endpoint_url' ) ? wc_get_account_endpoint_url( 'orders' ) : '', ]; $policy_snippets = []; foreach ( $pages as $label => $url ) { if ( ! $url ) { continue; } $policy_snippets[] = ucfirst( $label ) . ' URL: ' . esc_url_raw( $url ); } $manual = get_option( 'acosmaia_knowledge_text', '' ); if ( $manual !== '' ) { $policy_snippets[] = 'Manual policy note: ' . sanitize_textarea_field( $manual ); } return $this->truncate_context( implode( "\n", $policy_snippets ), 1800 ); } private function should_plan_product_query( $message_lower ) { if ( ! is_string( $message_lower ) || '' === $message_lower ) { return false; } return (bool) preg_match( '/(product|products|price|pricing|cost|budget|under|below|in stock|stock|available|compare|similar|recommend|suggest|buy|purchase|shipping|delivery|refund|return|category|brand|color|size|sale|discount|offer)/i', $message_lower ); } private function get_blocking_access_warning( $message_lower ) { $page_warning = $this->get_page_access_warning( $message_lower ); if ( '' !== $page_warning ) { return $page_warning; } $shop_warning = $this->get_shop_access_warning( $message_lower ); if ( '' !== $shop_warning ) { return $shop_warning; } return ''; } private function get_page_access_warning( $message_lower ) { if ( ! $this->looks_like_page_access_request( $message_lower ) ) { return ''; } $enabled_pages = $this->get_enabled_page_ids(); if ( empty( $enabled_pages ) ) { return 'Warning: Page access is not enabled. Enable pages in admin to allow AI to use page content.'; } $requested_pages = $this->get_requested_page_ids( $message_lower ); if ( empty( $requested_pages ) ) { return ''; } foreach ( $requested_pages as $requested_page_id ) { if ( in_array( $requested_page_id, $enabled_pages, true ) ) { return ''; } } return 'Warning: This page is not enabled for AI access. Enable it in admin to allow page content responses.'; } private function get_shop_access_warning( $message_lower ) { if ( ! $this->looks_like_product_request( $message_lower ) ) { return ''; } if ( ! function_exists( 'wc_get_page_id' ) ) { return ''; } $shop_page_id = (int) wc_get_page_id( 'shop' ); if ( $shop_page_id <= 0 ) { return ''; } $enabled_pages = $this->get_enabled_page_ids(); if ( in_array( $shop_page_id, $enabled_pages, true ) ) { return ''; } return 'Warning: Shop page access is disabled. Enable the Shop page in admin to allow product responses.'; } private function looks_like_product_request( $message_lower ) { if ( ! is_string( $message_lower ) || '' === $message_lower ) { return false; } return (bool) preg_match( '/\b(product|products|shop|catalog|catalogue|price|pricing|stock|available|compare|recommend|suggest|buy|purchase|cart|sku)\b/i', $message_lower ); } private function get_enabled_page_ids() { $enabled_pages = get_option( 'acosmaia_enabled_pages', [] ); if ( ! is_array( $enabled_pages ) ) { return []; } $ids = []; foreach ( $enabled_pages as $item ) { $id = absint( $item ); if ( $id > 0 ) { $ids[] = $id; } } return array_values( array_unique( $ids ) ); } private function get_requested_page_ids( $message_lower ) { if ( ! is_string( $message_lower ) || '' === $message_lower ) { return []; } $pages = get_pages( [ 'sort_column' => 'menu_order,post_title', 'post_status' => 'publish', 'number' => 150, ] ); if ( ! is_array( $pages ) || empty( $pages ) ) { return []; } $requested = []; foreach ( $pages as $page ) { if ( ! $page instanceof WP_Post ) { continue; } $title = strtolower( trim( (string) $page->post_title ) ); $slug = strtolower( trim( (string) $page->post_name ) ); if ( ( '' !== $title && strpos( $message_lower, $title ) !== false ) || ( '' !== $slug && strpos( $message_lower, $slug ) !== false ) ) { $requested[] = (int) $page->ID; } } return array_values( array_unique( $requested ) ); } private function looks_like_page_access_request( $message_lower ) { if ( ! is_string( $message_lower ) || '' === $message_lower ) { return false; } return (bool) preg_match( '/\b(page|pages|about|contact|policy|terms|privacy|shipping|refund|return|faq|support|company|who\s+are\s+you|about\s+us|contact\s+us)\b/i', $message_lower ); } private function should_lookup_products( $message_lower, array $query_plan ) { if ( isset( $query_plan['intent'] ) && is_string( $query_plan['intent'] ) && $query_plan['intent'] !== 'none' ) { return true; } if ( ! empty( $query_plan['search'] ) || ! empty( $query_plan['max_price'] ) || ! empty( $query_plan['in_stock_only'] ) || ! empty( $query_plan['compare_items'] ) ) { return true; } // If message still has meaningful search terms (for example, "show sunglass"), // do product lookup even when planner is skipped. if ( $this->extract_search_keywords( $message_lower ) !== '' ) { return true; } return $this->should_plan_product_query( $message_lower ); } private function generate_product_query_plan( $api_key, $message ) { $planner_prompt = "You convert a customer chat request into WooCommerce product query JSON.\n" . "Return JSON only with these keys: intent, search, max_price, in_stock_only, compare_items, sort_by.\n" . "intent values: search | compare | lowest_price | most_expensive | most_reviewed | none\n" . "sort_by values: none | price_asc | price_desc | rating_desc\n" . "compare_items must be an array of up to 2 product phrases when intent=compare.\n" . "If no price found, use null for max_price.\n" . "No markdown and no explanation, only JSON object."; $ai_client = ACOSMAIA_AI_API_Client::from_settings(); if ( ! $ai_client->is_configured() ) { return []; } $planner_response = $ai_client->request( $planner_prompt, (string) $message ); if ( is_wp_error( $planner_response ) ) { return []; } $parsed = $ai_client->parse_response( $planner_response ); if ( isset( $parsed['error'] ) ) { return []; } $response_content = isset( $parsed['content'] ) ? $parsed['content'] : ''; $response_usage = isset( $parsed['usage'] ) ? $parsed['usage'] : []; if ( empty( $response_content ) ) { return []; } $this->track_usage( $response_usage ); $raw_plan = $this->decode_json_from_text( $response_content ); if ( ! is_array( $raw_plan ) ) { return []; } $allowed_intents = [ 'search', 'compare', 'lowest_price', 'most_expensive', 'most_reviewed', 'none' ]; $allowed_sorts = [ 'none', 'price_asc', 'price_desc', 'rating_desc' ]; $intent = isset( $raw_plan['intent'] ) ? sanitize_text_field( strtolower( (string) $raw_plan['intent'] ) ) : 'none'; $sort_by = isset( $raw_plan['sort_by'] ) ? sanitize_text_field( strtolower( (string) $raw_plan['sort_by'] ) ) : 'none'; $compare_items = []; if ( isset( $raw_plan['compare_items'] ) && is_array( $raw_plan['compare_items'] ) ) { foreach ( array_slice( $raw_plan['compare_items'], 0, 2 ) as $item ) { $clean = sanitize_text_field( (string) $item ); if ( strlen( $clean ) >= 2 ) { $compare_items[] = $clean; } } } return [ 'intent' => in_array( $intent, $allowed_intents, true ) ? $intent : 'none', 'search' => isset( $raw_plan['search'] ) ? sanitize_text_field( (string) $raw_plan['search'] ) : '', 'max_price' => isset( $raw_plan['max_price'] ) && is_numeric( $raw_plan['max_price'] ) ? (float) $raw_plan['max_price'] : null, 'in_stock_only' => ! empty( $raw_plan['in_stock_only'] ), 'compare_items' => $compare_items, 'sort_by' => in_array( $sort_by, $allowed_sorts, true ) ? $sort_by : 'none', ]; } private function decode_json_from_text( $text ) { $trimmed = trim( (string) $text ); if ( '' === $trimmed ) { return null; } if ( preg_match( '/```(?:json)?\s*(\{[\s\S]*\})\s*```/i', $trimmed, $matches ) ) { $trimmed = trim( $matches[1] ); } $decoded = json_decode( $trimmed, true ); return is_array( $decoded ) ? $decoded : null; } private function get_fallback_models( $preferred_model, $api_key = '' ) { $dynamic_free_models = $this->get_openrouter_free_models( $api_key ); $candidates = array_merge( [ $preferred_model ], $dynamic_free_models, [ 'mistralai/mistral-7b-instruct:free', 'google/gemma-2-9b-it:free', 'meta-llama/llama-3.1-8b-instruct:free', ] ); $models = []; foreach ( $candidates as $candidate ) { $candidate = trim( (string) $candidate ); if ( '' === $candidate || in_array( $candidate, $models, true ) ) { continue; } $models[] = $candidate; } return $models; } private function get_openrouter_free_models( $api_key ) { $cache_key = 'acosmaia_openrouter_free_models'; $cached = get_transient( $cache_key ); if ( is_array( $cached ) && ! empty( $cached ) ) { return $cached; } $headers = [ 'Content-Type' => 'application/json', 'HTTP-Referer' => home_url( '/' ), 'X-Title' => get_bloginfo( 'name' ), ]; if ( $api_key ) { $headers['Authorization'] = 'Bearer ' . $api_key; } $response = wp_remote_get( 'https://openrouter.ai/api/v1/models', [ 'headers' => $headers, 'timeout' => 15, ] ); if ( is_wp_error( $response ) ) { return []; } $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( $status_code < 200 || $status_code > 299 ) { return []; } $body = json_decode( (string) wp_remote_retrieve_body( $response ), true ); if ( empty( $body['data'] ) || ! is_array( $body['data'] ) ) { return []; } $models = []; foreach ( $body['data'] as $item ) { if ( empty( $item['id'] ) || ! is_string( $item['id'] ) ) { continue; } $id = trim( $item['id'] ); if ( '' === $id || strpos( $id, ':free' ) === false ) { continue; } $models[] = $id; } if ( ! empty( $models ) ) { set_transient( $cache_key, $models, 6 * HOUR_IN_SECONDS ); } return $models; } private function find_relevant_products( $message, array $query_plan = [] ) { $message_lower = strtolower( $message ); $intent = isset( $query_plan['intent'] ) && is_string( $query_plan['intent'] ) && $query_plan['intent'] !== '' ? $query_plan['intent'] : $this->detect_product_query_intent( $message_lower ); $browse_request = $this->looks_like_browse_products_request( $message_lower ); // Extract meaningful keywords (remove stop words) $search_term = ''; if ( isset( $query_plan['search'] ) && is_string( $query_plan['search'] ) ) { $search_term = sanitize_text_field( $query_plan['search'] ); } if ( '' === $search_term ) { $search_term = $this->extract_search_keywords( $message_lower ); } // Generic browse/list requests should show product cards without forcing a text search. if ( $browse_request && in_array( $search_term, [ 'shop', 'catalog', 'catalogue', 'products', 'product' ], true ) ) { $search_term = ''; } $max_price = isset( $query_plan['max_price'] ) && is_numeric( $query_plan['max_price'] ) ? (float) $query_plan['max_price'] : $this->extract_max_price( $message ); $min_price = $this->extract_min_price( $message ); $in_stock_only = ! empty( $query_plan['in_stock_only'] ) || strpos( $message_lower, 'in stock' ) !== false || strpos( $message_lower, 'available' ) !== false; $compare_items = []; if ( isset( $query_plan['compare_items'] ) && is_array( $query_plan['compare_items'] ) ) { $compare_items = array_slice( $query_plan['compare_items'], 0, 2 ); } if ( empty( $compare_items ) && 'compare' === $intent ) { $compare_items = $this->extract_compare_items( $message ); } if ( 'compare' === $intent && count( $compare_items ) >= 2 ) { $compared_products = $this->find_products_for_comparison( $compare_items, $max_price, $in_stock_only ); if ( ! empty( $compared_products ) ) { return $compared_products; } } // Don't search if no meaningful keywords extracted and no product-specific intent detected. if ( empty( $search_term ) && null === $max_price && null === $min_price && 'none' === $intent && ! $browse_request ) { return []; } $args = [ 'limit' => 4, // Fetch one extra item to detect whether "show more" should be displayed. 'status' => 'publish', 'return' => 'objects', ]; if ( ! empty( $search_term ) ) { $args['s'] = $search_term; } if ( null !== $max_price ) { $args['max_price'] = (string) $max_price; } if ( null !== $min_price ) { $args['min_price'] = (string) $min_price; } $sort_by = isset( $query_plan['sort_by'] ) && is_string( $query_plan['sort_by'] ) ? $query_plan['sort_by'] : 'none'; if ( 'price_asc' === $sort_by || 'lowest_price' === $intent || ( null !== $max_price && empty( $search_term ) ) ) { $args['orderby'] = 'price'; $args['order'] = 'ASC'; } elseif ( 'price_desc' === $sort_by ) { $args['orderby'] = 'price'; $args['order'] = 'DESC'; } elseif ( 'rating_desc' === $sort_by || 'most_reviewed' === $intent ) { // WooCommerce supports rating sort; this is the closest built-in approximation. $args['orderby'] = 'rating'; $args['order'] = 'DESC'; } if ( $in_stock_only ) { $args['stock_status'] = 'instock'; } $products = wc_get_products( $args ); $products = $this->apply_price_and_stock_filters( $products, $min_price, $max_price, $in_stock_only ); if ( empty( $products ) && ! empty( $search_term ) ) { $products = $this->fallback_products_by_partial_name( $search_term, $max_price, $in_stock_only ); $products = $this->apply_price_and_stock_filters( $products, $min_price, $max_price, $in_stock_only ); } if ( empty( $products ) && ( null !== $min_price || null !== $max_price ) ) { $products = $this->fallback_products_by_price_range( $search_term, $min_price, $max_price, $in_stock_only ); } $has_more = count( $products ) > 3; $this->last_product_meta = [ 'has_more' => $has_more, 'show_more_url' => $has_more ? $this->resolve_show_more_url( $message_lower, $search_term ) : '', ]; if ( $has_more ) { $products = array_slice( $products, 0, 3 ); } return $products; } private function apply_price_and_stock_filters( array $products, $min_price = null, $max_price = null, $in_stock_only = false ) { $filtered = []; foreach ( $products as $product ) { if ( ! $product instanceof WC_Product ) { continue; } if ( $in_stock_only && ! $product->is_in_stock() ) { continue; } $price = (float) $product->get_price(); if ( null !== $min_price && $price < (float) $min_price ) { continue; } if ( null !== $max_price && $price > (float) $max_price ) { continue; } $filtered[] = $product; } return $filtered; } private function fallback_products_by_price_range( $search_term = '', $min_price = null, $max_price = null, $in_stock_only = false ) { $candidates = wc_get_products( [ 'limit' => 60, 'status' => 'publish', 'return' => 'objects', 'orderby' => 'date', 'order' => 'DESC', ] ); $filtered = $this->apply_price_and_stock_filters( $candidates, $min_price, $max_price, $in_stock_only ); $term = strtolower( trim( (string) $search_term ) ); if ( $term === '' ) { return array_slice( $filtered, 0, 4 ); } $matched = []; foreach ( $filtered as $product ) { if ( ! $product instanceof WC_Product ) { continue; } $haystack = strtolower( $product->get_name() . ' ' . $product->get_slug() ); if ( strpos( $haystack, $term ) === false ) { continue; } $matched[] = $product; if ( count( $matched ) >= 4 ) { break; } } return $matched; } private function get_show_more_url_for_products() { return isset( $this->last_product_meta['show_more_url'] ) ? esc_url_raw( (string) $this->last_product_meta['show_more_url'] ) : ''; } private function resolve_show_more_url( $message_lower, $search_term = '' ) { $matched_term_url = $this->find_matching_term_url( 'product_cat', $message_lower, $search_term ); if ( $matched_term_url !== '' ) { return $matched_term_url; } $matched_tag_url = $this->find_matching_term_url( 'product_tag', $message_lower, $search_term ); if ( $matched_tag_url !== '' ) { return $matched_tag_url; } $shop_url = function_exists( 'wc_get_page_permalink' ) ? wc_get_page_permalink( 'shop' ) : ''; return $shop_url ? esc_url_raw( $shop_url ) : esc_url_raw( home_url( '/' ) ); } private function find_matching_term_url( $taxonomy, $message_lower, $search_term = '' ) { $terms = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => true, 'number' => 80, ] ); if ( is_wp_error( $terms ) || ! is_array( $terms ) ) { return ''; } $haystack = strtolower( trim( $message_lower . ' ' . $search_term ) ); if ( $haystack === '' ) { return ''; } foreach ( $terms as $term ) { if ( ! $term instanceof WP_Term ) { continue; } $name = strtolower( trim( (string) $term->name ) ); $slug = strtolower( trim( (string) $term->slug ) ); if ( ( $name !== '' && strpos( $haystack, $name ) !== false ) || ( $slug !== '' && strpos( $haystack, $slug ) !== false ) ) { $url = get_term_link( $term ); return is_wp_error( $url ) ? '' : esc_url_raw( $url ); } } return ''; } private function extract_compare_items( $message ) { if ( preg_match( '/compare\s+(.+?)\s+(?:and|vs|versus)\s+(.+)/i', $message, $matches ) ) { $left = sanitize_text_field( trim( $matches[1], " \t\n\r\0\x0B\"'" ) ); $right = sanitize_text_field( trim( $matches[2], " \t\n\r\0\x0B\"'" ) ); $items = []; if ( strlen( $left ) >= 2 ) { $items[] = $left; } if ( strlen( $right ) >= 2 ) { $items[] = $right; } return $items; } return []; } private function find_products_for_comparison( array $compare_items, $max_price = null, $in_stock_only = false ) { $products = []; foreach ( array_slice( $compare_items, 0, 2 ) as $item ) { $term = sanitize_text_field( (string) $item ); if ( '' === $term ) { continue; } $args = [ 'limit' => 1, 'status' => 'publish', 'return' => 'objects', 's' => $term, ]; if ( null !== $max_price ) { $args['max_price'] = (string) $max_price; } if ( $in_stock_only ) { $args['stock_status'] = 'instock'; } $matches = wc_get_products( $args ); if ( empty( $matches ) || ! $matches[0] instanceof WC_Product ) { continue; } $product = $matches[0]; $product_id = $product->get_id(); if ( isset( $products[ $product_id ] ) ) { continue; } $products[ $product_id ] = $product; } return array_values( $products ); } private function detect_product_query_intent( $message_lower ) { if ( preg_match( '/(compare|\svs\s|versus)/i', $message_lower ) ) { return 'compare'; } if ( preg_match( '/(most\s+reviewed|top\s+reviewed|highest\s+rated|best\s+rated)/i', $message_lower ) ) { return 'most_reviewed'; } if ( preg_match( '/(lowest|cheapest|least\s+expensive|minimum).*(price|pricing|cost)|lowest\s+pricing/i', $message_lower ) ) { return 'lowest_price'; } return 'none'; } private function fallback_products_by_partial_name( $search_term, $max_price = null, $in_stock_only = false ) { $term = strtolower( sanitize_text_field( (string) $search_term ) ); if ( '' === $term ) { return []; } $needles = [ $term ]; if ( substr( $term, -2 ) === 'es' && strlen( $term ) > 4 ) { $needles[] = substr( $term, 0, -2 ); } if ( substr( $term, -1 ) === 's' && strlen( $term ) > 3 ) { $needles[] = substr( $term, 0, -1 ); } $candidates = wc_get_products( [ 'limit' => 40, 'status' => 'publish', 'return' => 'objects', 'orderby' => 'date', 'order' => 'DESC', ] ); $matched = []; foreach ( $candidates as $product ) { if ( ! $product instanceof WC_Product ) { continue; } $price = (float) $product->get_price(); if ( null !== $max_price && $price > (float) $max_price ) { continue; } if ( $in_stock_only && ! $product->is_in_stock() ) { continue; } $haystack = strtolower( $product->get_name() . ' ' . $product->get_slug() ); $is_match = false; foreach ( $needles as $needle ) { if ( $needle !== '' && strpos( $haystack, $needle ) !== false ) { $is_match = true; break; } } if ( ! $is_match ) { continue; } $matched[] = $product; if ( count( $matched ) >= 3 ) { break; } } return $matched; } private function extract_search_keywords( $message_lower ) { // Common stop words to remove $stop_words = [ 'can', 'you', 'i', 'me', 'my', 'we', 'he', 'she', 'it', 'they', 'them', 'is', 'are', 'am', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'from', 'with', 'by', 'suggest', 'show', 'find', 'give', 'get', 'have', 'recommend', 'help', 'want', 'need', 'please', 'thanks', 'thank', 'hi', 'hello', 'hey', 'ok', 'okay', 'price', 'pricing', 'cost', 'under', 'below', 'budget', 'product', 'products', 'site', 'shop', 'catalog', 'catalogue', 'lowest', 'cheapest', 'most', 'reviewed', 'reviews', 'review', 'rated', 'over', 'above', 'greater', 'greaterthan', 'more', 'than', 'higher', 'rupee', 'rupees', 'inr', 'rs', ]; // Remove punctuation and split into words $words = preg_split( '/[\s\-_,\.!?]+/', trim( $message_lower ), -1, PREG_SPLIT_NO_EMPTY ); // Filter out stop words and keep only meaningful keywords $keywords = []; foreach ( $words as $word ) { $word = trim( $word ); if ( ! empty( $word ) && preg_match( '/^[0-9]+(?:\.[0-9]+)?$/', $word ) ) { continue; } if ( ! empty( $word ) && strlen( $word ) > 2 && ! in_array( $word, $stop_words, true ) ) { $keywords[] = $word; } } // Return keywords joined by space (WooCommerce will search for all terms) return ! empty( $keywords ) ? implode( ' ', array_slice( $keywords, 0, 3 ) ) : ''; } private function extract_max_price( $message ) { $patterns = [ '/(?:under|below|less\s+than|upto|up\s*to)\s*(?:rs\.?|rupees?|inr|₹|\$)?\s*([0-9]+(?:\.[0-9]+)?)/i', '/(?:rs\.?|rupees?|inr|₹|\$)\s*([0-9]+(?:\.[0-9]+)?)/i', '/([0-9]+(?:\.[0-9]+)?)\s*(?:rs\.?|rupees?|inr|₹|\$)/i', ]; foreach ( $patterns as $pattern ) { if ( preg_match( $pattern, $message, $matches ) ) { return (float) $matches[1]; } } return null; } private function extract_min_price( $message ) { $patterns = [ '/(?:over|above|greater\s*than|more\s*than|higher\s*than)\s*(?:rs\.?|rupees?|inr|₹|\$|₪)?\s*([0-9]+(?:\.[0-9]+)?)/i', '/(?:rs\.?|rupees?|inr|₹|\$|₪)\s*([0-9]+(?:\.[0-9]+)?)\s*(?:and\s*above|\+\s*$)/i', ]; foreach ( $patterns as $pattern ) { if ( preg_match( $pattern, $message, $matches ) ) { return (float) $matches[1]; } } return null; } private function looks_like_budget_query( $message_lower ) { return (bool) preg_match( '/(under|below|less than|upto|up to|budget|cheap|affordable|price|cost|rs\.?|rupees?|inr|₹|\$)/i', $message_lower ); } private function looks_like_browse_products_request( $message_lower ) { return (bool) preg_match( '/\b(show|list|browse|view|see)\b.*\b(product|products|shop|catalog|catalogue)\b|\b(all|shop)\s+products?\b/i', $message_lower ); } private function build_product_cards( array $products ) { $cards = []; foreach ( $products as $product ) { if ( ! $product instanceof WC_Product ) { continue; } $cards[] = [ 'id' => $product->get_id(), 'name' => $product->get_name(), 'description' => wp_strip_all_tags( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ), 'price' => wp_strip_all_tags( wc_price( (float) $product->get_price() ) ), 'sale_price' => $product->get_sale_price() !== '' ? wp_strip_all_tags( wc_price( (float) $product->get_sale_price() ) ) : '', 'average_rating' => (float) $product->get_average_rating(), 'review_count' => (int) $product->get_review_count(), 'sku' => $product->get_sku(), 'stock_status' => $product->is_in_stock() ? 'instock' : 'outofstock', 'categories' => wp_strip_all_tags( wc_get_product_category_list( $product->get_id(), ', ', '', '' ) ), 'tags' => wp_strip_all_tags( wc_get_product_tag_list( $product->get_id(), ', ', '', '' ) ), 'attributes' => $this->format_attributes( $product ), 'image' => wp_get_attachment_image_url( $product->get_image_id(), 'woocommerce_thumbnail' ), 'url' => $product->get_permalink(), 'add_to_cart_url' => $this->get_card_add_to_cart_url( $product ), ]; } return $cards; } private function get_card_add_to_cart_url( WC_Product $product ) { // Show Add to Cart only for directly purchasable simple products. if ( ! $product->is_type( 'simple' ) ) { return ''; } if ( ! $product->is_purchasable() || ! $product->is_in_stock() ) { return ''; } return $product->add_to_cart_url(); } private function format_attributes( WC_Product $product ) { $result = []; foreach ( $product->get_attributes() as $attribute ) { if ( ! $attribute instanceof WC_Product_Attribute ) { continue; } $name = wc_attribute_label( $attribute->get_name() ); if ( $attribute->is_taxonomy() ) { $terms = wp_get_post_terms( $product->get_id(), $attribute->get_name(), [ 'fields' => 'names' ] ); $value = is_wp_error( $terms ) ? [] : $terms; } else { $value = $attribute->get_options(); } $result[] = [ 'name' => $name, 'value' => implode( ', ', array_map( 'sanitize_text_field', (array) $value ) ), ]; } return $result; } private function fallback_message( array $products, $message, $no_api = false ) { $base = $no_api ? 'AI API key is not configured yet. Here are products that may help:' : get_option( 'acosmaia_fallback_response', 'Sorry, I am unable to answer right now. Please contact support.' ); if ( empty( $products ) ) { return $base . ' I could not find matching products for: ' . $message; } return $base; } private function is_order_details_request( $message_lower ) { if ( ! is_string( $message_lower ) || '' === trim( $message_lower ) ) { return false; } return (bool) preg_match( '/\b(my\s+orders?|order\s+history|recent\s+orders?|recent\s+purchases?|purchase\s+history|order\s+details?|track\s+my\s+order|show\s+my\s+orders?|show\s+my\s+recent\s+purchase\s+history)\b/i', $message_lower ); } private function build_current_user_order_details_message() { if ( ! function_exists( 'is_user_logged_in' ) || ! is_user_logged_in() ) { $login_url = function_exists( 'wc_get_page_permalink' ) ? wc_get_page_permalink( 'myaccount' ) : wp_login_url(); $login_url = $login_url ? esc_url_raw( $login_url ) : ''; return $login_url ? 'Please log in to view your order details: ' . $login_url : 'Please log in to view your order details.'; } $current_user_id = get_current_user_id(); if ( $current_user_id <= 0 || ! function_exists( 'wc_get_orders' ) ) { return 'Unable to load your order details right now.'; } $orders = wc_get_orders( [ 'customer_id' => $current_user_id, 'limit' => 5, 'orderby' => 'date', 'order' => 'DESC', 'return' => 'objects', ] ); if ( empty( $orders ) ) { return 'No orders found for your account.'; } $lines = [ 'Your recent orders:' ]; foreach ( $orders as $order ) { if ( ! $order instanceof WC_Order ) { continue; } $order_number = '#' . sanitize_text_field( (string) $order->get_order_number() ); $order_date = $order->get_date_created(); $date_text = $order_date instanceof WC_DateTime ? $order_date->date_i18n( get_option( 'date_format', 'Y-m-d' ) ) : ''; $status = sanitize_text_field( wc_get_order_status_name( $order->get_status() ) ); $total = wp_strip_all_tags( $order->get_formatted_order_total() ); $item_count = (int) $order->get_item_count(); $line = sprintf( '%s | %s | %s | %s | %d item%s', $order_number, $date_text !== '' ? $date_text : 'Date unavailable', $status !== '' ? $status : 'Unknown status', $total !== '' ? $total : 'Total unavailable', $item_count, $item_count === 1 ? '' : 's' ); $lines[] = $line; } if ( 1 === count( $lines ) ) { return 'No orders found for your account.'; } if ( function_exists( 'wc_get_account_endpoint_url' ) ) { $orders_url = wc_get_account_endpoint_url( 'orders' ); if ( is_string( $orders_url ) && $orders_url !== '' ) { $lines[] = 'View all orders: ' . esc_url_raw( $orders_url ); } } return implode( "\n", $lines ); } }