server = $server; } /** * Set the agent bridge (resolves late binding). * * @param AgentBridge $bridge The agent bridge instance. * * @return void */ public function set_bridge( AgentBridge $bridge ): void { $this->bridge = $bridge; } /** * Get tool definitions for tools/list. * * @return array */ public function get_tool_definitions(): array { return array( $this->def_search_products(), $this->def_get_product_details(), $this->def_check_inventory(), $this->def_get_price(), $this->def_get_categories(), $this->def_get_store_info(), $this->def_initiate_checkout(), $this->def_get_checkout_status(), ); } // ─── Tool definitions ────────────────────────────────────────────── /** * Definition for search_products. * * @return array */ private function def_search_products(): array { return array( 'name' => 'search_products', 'description' => 'Search the WooCommerce product catalog. Returns matching products with price, availability, and key details. Use this to find products before getting full details or initiating checkout.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'query' => array( 'type' => 'string', 'description' => 'Search terms, e.g. \'blue coffee mug under $30\'', ), 'category' => array( 'type' => 'string', 'description' => 'Product category name or slug to filter by', ), 'min_price' => array( 'type' => 'number', 'description' => 'Minimum price in store currency', ), 'max_price' => array( 'type' => 'number', 'description' => 'Maximum price in store currency', ), 'in_stock_only' => array( 'type' => 'boolean', 'description' => 'Only return in-stock products', 'default' => true, ), 'limit' => array( 'type' => 'integer', 'description' => 'Max results to return (1-20)', 'default' => 10, 'minimum' => 1, 'maximum' => 20, ), ), 'required' => array(), ), ); } /** * Definition for get_product_details. * * @return array */ private function def_get_product_details(): array { return array( 'name' => 'get_product_details', 'description' => 'Get complete details for a specific product including all variants, attributes, images, FAQs, and customer reviews summary.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'product_id' => array( 'type' => 'integer', 'description' => 'WooCommerce product ID', ), ), 'required' => array( 'product_id' ), ), ); } /** * Definition for check_inventory. * * @return array */ private function def_check_inventory(): array { return array( 'name' => 'check_inventory', 'description' => 'Check real-time stock availability for a product or specific variant. Always call this before attempting to initiate a purchase.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'product_id' => array( 'type' => 'integer', 'description' => 'WooCommerce product ID', ), 'variation_id' => array( 'type' => 'integer', 'description' => 'For variable products — specific variation ID', ), ), 'required' => array( 'product_id' ), ), ); } /** * Definition for get_price. * * @return array */ private function def_get_price(): array { return array( 'name' => 'get_price', 'description' => 'Get current pricing for a product including sale prices and bulk/quantity discounts if available.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'product_id' => array( 'type' => 'integer', 'description' => 'WooCommerce product ID', ), 'variation_id' => array( 'type' => 'integer', 'description' => 'Specific variation ID', ), 'quantity' => array( 'type' => 'integer', 'description' => 'Check price for this quantity (for bulk pricing)', 'default' => 1, ), ), 'required' => array( 'product_id' ), ), ); } /** * Definition for get_categories. * * @return array */ private function def_get_categories(): array { return array( 'name' => 'get_categories', 'description' => 'Browse the product category tree. Use to understand store taxonomy before searching.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'parent_id' => array( 'type' => 'integer', 'description' => 'Get subcategories of this category ID. Omit for top-level categories.', 'default' => 0, ), 'include_product_count' => array( 'type' => 'boolean', 'default' => true, ), ), 'required' => array(), ), ); } /** * Definition for get_store_info. * * @return array */ private function def_get_store_info(): array { return array( 'name' => 'get_store_info', 'description' => 'Get general store information including currency, shipping destinations, return policy, and accepted payment methods.', 'inputSchema' => array( 'type' => 'object', 'properties' => (object) array(), 'required' => array(), ), ); } /** * Definition for initiate_checkout. * * @return array */ private function def_initiate_checkout(): array { return array( 'name' => 'initiate_checkout', 'description' => 'Initiate a checkout session for specified products. Returns a checkout URL for the user to complete payment. The session expires in 10 minutes.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'items' => array( 'type' => 'array', 'description' => 'Array of items to purchase', 'items' => array( 'type' => 'object', 'properties' => array( 'product_id' => array( 'type' => 'integer', 'description' => 'WooCommerce product ID', ), 'variation_id' => array( 'type' => 'integer', 'description' => 'Variation ID for variable products', ), 'quantity' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, ), ), 'required' => array( 'product_id' ), ), ), ), 'required' => array( 'items' ), ), ); } /** * Definition for get_checkout_status. * * @return array */ private function def_get_checkout_status(): array { return array( 'name' => 'get_checkout_status', 'description' => 'Check the status of a previously initiated checkout session.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'session_id' => array( 'type' => 'string', 'description' => 'Session ID returned by initiate_checkout', ), ), 'required' => array( 'session_id' ), ), ); } // ─── Tool implementations ────────────────────────────────────────── /** * Search WooCommerce products. * * @param string $id JSON-RPC request ID. * @param array $args Tool arguments. * * @return \WP_REST_Response */ public function search_products( string $id, array $args ): \WP_REST_Response { $query = sanitize_text_field( $args['query'] ?? '' ); $category = sanitize_text_field( $args['category'] ?? '' ); $min_price = isset( $args['min_price'] ) ? (float) $args['min_price'] : null; $max_price = isset( $args['max_price'] ) ? (float) $args['max_price'] : null; $in_stock = isset( $args['in_stock_only'] ) ? (bool) $args['in_stock_only'] : true; $limit = min( 20, max( 1, (int) ( $args['limit'] ?? 10 ) ) ); $query_args = array( 'status' => 'publish', 'limit' => $limit, 'orderby' => 'relevance', ); if ( $query ) { $query_args['s'] = $query; } if ( $in_stock ) { $query_args['stock_status'] = 'instock'; } if ( null !== $min_price ) { $query_args['min_price'] = $min_price; } if ( null !== $max_price ) { $query_args['max_price'] = $max_price; } if ( $category ) { $term = get_term_by( 'name', $category, 'product_cat' ); if ( ! $term ) { $term = get_term_by( 'slug', $category, 'product_cat' ); } if ( $term ) { $query_args['category'] = array( $term->slug ); } } $wc_query = new \WC_Product_Query( $query_args ); $products = $wc_query->get_products(); $results = array(); foreach ( $products as $product ) { $results[] = array( 'id' => $product->get_id(), 'title' => $product->get_name(), 'price' => (float) $product->get_price(), 'regular_price' => (float) $product->get_regular_price(), 'sale_price' => $product->is_on_sale() ? (float) $product->get_sale_price() : null, 'on_sale' => $product->is_on_sale(), 'currency' => get_woocommerce_currency(), 'availability' => $product->is_in_stock() ? 'in_stock' : 'out_of_stock', 'url' => get_permalink( $product->get_id() ), 'image_url' => wp_get_attachment_image_url( $product->get_image_id(), 'woocommerce_single' ) ?: null, 'short_description' => wp_strip_all_tags( $product->get_short_description() ), 'category' => implode( ', ', wp_list_pluck( wp_get_post_terms( $product->get_id(), 'product_cat' ), 'name' ) ), 'rating' => array( 'value' => (float) $product->get_average_rating(), 'count' => (int) $product->get_review_count(), ), 'sku' => $product->get_sku(), 'type' => $product->get_type(), ); unset( $product ); } return $this->server->tool_result( $id, array( 'found' => count( $results ), 'products' => $results, 'currency' => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD', 'query' => $query ?: null, ) ); } /** * Get detailed product information. * * @param string $id JSON-RPC request ID. * @param array $args Tool arguments. * * @return \WP_REST_Response */ public function get_product_details( string $id, array $args ): \WP_REST_Response { $product_id = absint( $args['product_id'] ?? 0 ); $product = wc_get_product( $product_id ); if ( ! $product || 'publish' !== $product->get_status() ) { return $this->server->error_response( $id, -32602, "Product {$product_id} not found or not published" ); } $extractor = new ProductDataExtractor(); $data = $extractor->extract( $product ); // Add AI-optimized content if active. if ( 'yes' === get_post_meta( $product_id, '_aicommerce_use_optimized_description', true ) ) { $optimized = get_post_meta( $product_id, '_aicommerce_optimized_description', true ); if ( $optimized ) { $data['description'] = $optimized; } } // Add FAQs if active. if ( 'yes' === get_post_meta( $product_id, '_aicommerce_use_optimized_faqs', true ) ) { $faqs_raw = get_post_meta( $product_id, '_aicommerce_faqs', true ); $data['faqs'] = $faqs_raw ? json_decode( $faqs_raw, true ) : array(); } // Add review summary. $data['reviews'] = array( 'average' => (float) $product->get_average_rating(), 'count' => (int) $product->get_review_count(), 'summary' => $this->get_review_summary( $product_id ), ); return $this->server->tool_result( $id, $data ); } /** * Check real-time stock availability. * * @param string $id JSON-RPC request ID. * @param array $args Tool arguments. * * @return \WP_REST_Response */ public function check_inventory( string $id, array $args ): \WP_REST_Response { $product_id = absint( $args['product_id'] ?? 0 ); $variation_id = absint( $args['variation_id'] ?? 0 ); $product = $variation_id ? wc_get_product( $variation_id ) : wc_get_product( $product_id ); if ( ! $product ) { $check_id = $variation_id ?: $product_id; return $this->server->error_response( $id, -32602, "Product {$check_id} not found" ); } return $this->server->tool_result( $id, array( 'product_id' => $product_id, 'variation_id' => $variation_id ?: null, 'in_stock' => $product->is_in_stock(), 'stock_status' => $product->get_stock_status(), 'stock_quantity' => $product->get_manage_stock() ? $product->get_stock_quantity() : null, 'backorders_allowed' => $product->backorders_allowed(), 'sold_individually' => $product->is_sold_individually(), 'checked_at' => current_time( 'c' ), ) ); } /** * Get current pricing for a product. * * @param string $id JSON-RPC request ID. * @param array $args Tool arguments. * * @return \WP_REST_Response */ public function get_price( string $id, array $args ): \WP_REST_Response { $product_id = absint( $args['product_id'] ?? 0 ); $variation_id = absint( $args['variation_id'] ?? 0 ); $quantity = max( 1, (int) ( $args['quantity'] ?? 1 ) ); $product = $variation_id ? wc_get_product( $variation_id ) : wc_get_product( $product_id ); if ( ! $product ) { $check_id = $variation_id ?: $product_id; return $this->server->error_response( $id, -32602, "Product {$check_id} not found" ); } $price = wc_get_price_including_tax( $product, array( 'qty' => $quantity ) ); $regular_price = wc_get_price_including_tax( $product, array( 'qty' => $quantity, 'price' => $product->get_regular_price(), ) ); $sale_price = null; if ( $product->is_on_sale() ) { $sale_price = (float) wc_get_price_including_tax( $product, array( 'price' => $product->get_sale_price(), ) ); } return $this->server->tool_result( $id, array( 'product_id' => $product_id, 'variation_id' => $variation_id ?: null, 'price' => (float) $price, 'regular_price' => (float) $regular_price, 'sale_price' => $sale_price, 'on_sale' => $product->is_on_sale(), 'currency' => get_woocommerce_currency(), 'currency_symbol' => get_woocommerce_currency_symbol(), 'tax_included' => wc_prices_include_tax(), 'quantity' => $quantity, 'checked_at' => current_time( 'c' ), ) ); } /** * Browse the product category tree. * * @param string $id JSON-RPC request ID. * @param array $args Tool arguments. * * @return \WP_REST_Response */ public function get_categories( string $id, array $args ): \WP_REST_Response { $parent_id = absint( $args['parent_id'] ?? 0 ); $terms = get_terms( array( 'taxonomy' => 'product_cat', 'parent' => $parent_id, 'hide_empty' => true, 'number' => 100, ) ); if ( is_wp_error( $terms ) ) { return $this->server->error_response( $id, -32603, 'Failed to retrieve categories' ); } $cats = array(); foreach ( $terms as $term ) { $child_count = get_terms( array( 'taxonomy' => 'product_cat', 'parent' => $term->term_id, 'count' => true, 'fields' => 'count', ) ); $cats[] = array( 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'product_count' => $term->count, 'has_children' => is_numeric( $child_count ) && (int) $child_count > 0, ); } return $this->server->tool_result( $id, array( 'parent_id' => $parent_id, 'categories' => $cats, 'total' => count( $cats ), ) ); } /** * Get general store information. * * @param string $id JSON-RPC request ID. * @param array $args Tool arguments (unused). * * @return \WP_REST_Response */ public function get_store_info( string $id, array $args ): \WP_REST_Response { $gateway_names = array(); if ( function_exists( 'WC' ) && WC()->payment_gateways() ) { $gateways = WC()->payment_gateways()->get_available_payment_gateways(); foreach ( $gateways as $gateway ) { $gateway_names[] = $gateway->get_title(); } } $shipping_countries = array(); if ( function_exists( 'WC' ) && WC()->countries ) { $shipping_countries = array_keys( WC()->countries->get_shipping_countries() ); } $total_products = 0; $counts = wp_count_posts( 'product' ); if ( $counts ) { $total_products = (int) $counts->publish; } return $this->server->tool_result( $id, array( 'store_name' => get_bloginfo( 'name' ), 'store_url' => home_url(), 'currency' => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD', 'currency_symbol' => function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '$', 'price_format' => function_exists( 'get_woocommerce_price_format' ) ? get_woocommerce_price_format() : '%1$s%2$s', 'weight_unit' => get_option( 'woocommerce_weight_unit', 'kg' ), 'dimension_unit' => get_option( 'woocommerce_dimension_unit', 'cm' ), 'shipping_countries' => $shipping_countries, 'payment_methods' => $gateway_names, 'return_policy_url' => get_option( 'aicommerce_return_policy_url' ) ?: null, 'privacy_policy_url' => function_exists( 'get_privacy_policy_url' ) ? ( get_privacy_policy_url() ?: null ) : null, 'terms_url' => get_option( 'aicommerce_terms_url' ) ?: null, 'total_products' => $total_products, ) ); } /** * Initiate a checkout session. * * @param string $id JSON-RPC request ID. * @param array $args Tool arguments. * * @return \WP_REST_Response */ public function initiate_checkout( string $id, array $args ): \WP_REST_Response { if ( ! $this->bridge ) { return $this->server->error_response( $id, -32603, 'Checkout service not available' ); } try { $result = $this->bridge->initiate_checkout( $args ); return $this->server->tool_result( $id, $result ); } catch ( \InvalidArgumentException $e ) { return $this->server->error_response( $id, -32602, $e->getMessage() ); } catch ( \RuntimeException $e ) { return $this->server->error_response( $id, -32603, $e->getMessage() ); } } /** * Get checkout session status. * * @param string $id JSON-RPC request ID. * @param array $args Tool arguments. * * @return \WP_REST_Response */ public function get_checkout_status( string $id, array $args ): \WP_REST_Response { if ( ! $this->bridge ) { return $this->server->error_response( $id, -32603, 'Checkout service not available' ); } $session_id = sanitize_text_field( $args['session_id'] ?? '' ); if ( empty( $session_id ) ) { return $this->server->error_response( $id, -32602, 'session_id is required' ); } try { $result = $this->bridge->get_checkout_status( $session_id ); return $this->server->tool_result( $id, $result ); } catch ( \InvalidArgumentException $e ) { return $this->server->error_response( $id, -32602, $e->getMessage() ); } } // ─── Private helpers ─────────────────────────────────────────────── /** * Get a brief review summary (last 3 reviews, truncated). * * @param int $product_id Product ID. * * @return array Array of recent review snippets. */ private function get_review_summary( int $product_id ): array { $comments = get_comments( array( 'post_id' => $product_id, 'type' => 'review', 'status' => 'approve', 'number' => 3, 'orderby' => 'comment_date_gmt', 'order' => 'DESC', ) ); $summaries = array(); foreach ( $comments as $comment ) { $text = wp_strip_all_tags( $comment->comment_content ); if ( strlen( $text ) > 80 ) { $text = substr( $text, 0, 80 ) . '…'; } $rating = get_comment_meta( $comment->comment_ID, 'rating', true ); $summaries[] = array( 'rating' => $rating ? (int) $rating : null, 'text' => $text, ); } return $summaries; } }