Updating product AI features endpoints (#38930)
This commit is contained in:
parent
a99ce61fb2
commit
419bd98db7
|
@ -1,23 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* API initialization for ai plugin.
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API initialization for ai plugin.
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the Woo AI route.
|
|
||||||
*/
|
|
||||||
function woo_ai_rest_api_init() {
|
|
||||||
require_once dirname( __FILE__ ) . '/product-data-suggestion/class-product-data-suggestion-api.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
add_action( 'rest_api_init', 'woo_ai_rest_api_init' );
|
|
|
@ -1,198 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* REST API Attribute Suggestion Controller
|
|
||||||
*
|
|
||||||
* Handles requests to /wc-admin/wooai/product-data-suggestions
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\Admin\API;
|
|
||||||
|
|
||||||
use Automattic\WooCommerce\AI\Completion\Jetpack_Completion_Service;
|
|
||||||
use Automattic\WooCommerce\AI\ProductDataSuggestion\Product_Data_Suggestion_Exception;
|
|
||||||
use Automattic\WooCommerce\AI\ProductDataSuggestion\Product_Data_Suggestion_Prompt_Generator;
|
|
||||||
use Automattic\WooCommerce\AI\ProductDataSuggestion\Product_Data_Suggestion_Request;
|
|
||||||
use Automattic\WooCommerce\AI\ProductDataSuggestion\Product_Data_Suggestion_Service;
|
|
||||||
use Automattic\WooCommerce\AI\PromptFormatter\Json_Request_Formatter;
|
|
||||||
use Automattic\WooCommerce\AI\PromptFormatter\Product_Attribute_Formatter;
|
|
||||||
use Automattic\WooCommerce\AI\PromptFormatter\Product_Category_Formatter;
|
|
||||||
use WC_REST_Data_Controller;
|
|
||||||
use WP_Error;
|
|
||||||
use WP_HTTP_Response;
|
|
||||||
use WP_REST_Request;
|
|
||||||
use WP_REST_Response;
|
|
||||||
use WP_REST_Server;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attribute Suggestion API controller.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* @extends WC_REST_Data_Controller
|
|
||||||
*/
|
|
||||||
class Product_Data_Suggestion_API extends WC_REST_Data_Controller {
|
|
||||||
/**
|
|
||||||
* The product data suggestion service.
|
|
||||||
*
|
|
||||||
* @var Product_Data_Suggestion_Service
|
|
||||||
*/
|
|
||||||
protected $product_data_suggestion_service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Endpoint namespace.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $namespace = 'wooai';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Route base.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $rest_base = 'product-data-suggestions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
public function __construct() {
|
|
||||||
$json_request_formatter = new Json_Request_Formatter();
|
|
||||||
$product_category_formatter = new Product_Category_Formatter();
|
|
||||||
$product_attribute_formatter = new Product_Attribute_Formatter();
|
|
||||||
$prompt_generator = new Product_Data_Suggestion_Prompt_Generator( $product_category_formatter, $product_attribute_formatter, $json_request_formatter );
|
|
||||||
$completion_service = new Jetpack_Completion_Service();
|
|
||||||
|
|
||||||
$this->product_data_suggestion_service = new Product_Data_Suggestion_Service( $prompt_generator, $completion_service );
|
|
||||||
|
|
||||||
$this->register_routes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register routes.
|
|
||||||
*/
|
|
||||||
public function register_routes() {
|
|
||||||
register_rest_route(
|
|
||||||
$this->namespace,
|
|
||||||
'/' . $this->rest_base,
|
|
||||||
array(
|
|
||||||
array(
|
|
||||||
'methods' => WP_REST_Server::CREATABLE,
|
|
||||||
'callback' => array( $this, 'get_response' ),
|
|
||||||
'permission_callback' => array( $this, 'get_response_permission_check' ),
|
|
||||||
'args' => array(
|
|
||||||
'requested_data' => array(
|
|
||||||
'type' => 'string',
|
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
|
||||||
'required' => true,
|
|
||||||
),
|
|
||||||
'name' => array(
|
|
||||||
'type' => 'string',
|
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
|
||||||
'default' => '',
|
|
||||||
),
|
|
||||||
'description' => array(
|
|
||||||
'type' => 'string',
|
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
|
||||||
'default' => '',
|
|
||||||
),
|
|
||||||
'categories' => array(
|
|
||||||
'type' => 'array',
|
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
|
||||||
'sanitize_callback' => 'wp_parse_id_list',
|
|
||||||
'items' => array(
|
|
||||||
'type' => 'integer',
|
|
||||||
),
|
|
||||||
'default' => array(),
|
|
||||||
),
|
|
||||||
'tags' => array(
|
|
||||||
'type' => 'array',
|
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
|
||||||
'items' => array(
|
|
||||||
'type' => 'string',
|
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
|
||||||
),
|
|
||||||
'default' => array(),
|
|
||||||
),
|
|
||||||
'attributes' => array(
|
|
||||||
'type' => 'array',
|
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
|
||||||
'default' => array(),
|
|
||||||
'items' => array(
|
|
||||||
'type' => 'object',
|
|
||||||
'properties' => array(
|
|
||||||
'key' => array(
|
|
||||||
'type' => 'string',
|
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
|
||||||
),
|
|
||||||
'value' => array(
|
|
||||||
'type' => 'string',
|
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a given request has access to create a product.
|
|
||||||
*
|
|
||||||
* @param WP_REST_Request $request Full details about the request.
|
|
||||||
*
|
|
||||||
* @return WP_Error|boolean
|
|
||||||
*/
|
|
||||||
public function get_response_permission_check( WP_REST_Request $request ) {
|
|
||||||
if ( ! wc_rest_check_post_permissions( 'product', 'create' ) ) {
|
|
||||||
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the product-data suggestions.
|
|
||||||
*
|
|
||||||
* @param WP_REST_Request $request Full details about the request.
|
|
||||||
*
|
|
||||||
* @return WP_Error|WP_HTTP_Response|WP_REST_Response
|
|
||||||
*/
|
|
||||||
public function get_response( WP_REST_Request $request ) {
|
|
||||||
$requested_data = $request->get_param( 'requested_data' );
|
|
||||||
$name = $request->get_param( 'name' );
|
|
||||||
$description = $request->get_param( 'description' );
|
|
||||||
$categories = $request->get_param( 'categories' );
|
|
||||||
$tags = $request->get_param( 'tags' );
|
|
||||||
$attributes = $request->get_param( 'attributes' );
|
|
||||||
|
|
||||||
// Strip HTML tags from the description.
|
|
||||||
if ( ! empty( $description ) ) {
|
|
||||||
$description = wp_strip_all_tags( $request->get_param( 'description' ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if enough data is provided in the name and description to get suggestions.
|
|
||||||
if ( strlen( $name ) < 10 && strlen( $description ) < 50 ) {
|
|
||||||
return new WP_Error( 'error', __( 'Enter a few descriptive words or add product description, tags, or attributes to generate name ideas.', 'woocommerce' ), array( 'status' => 400 ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$product_data_request = new Product_Data_Suggestion_Request( $requested_data, $name, $description, $tags, $categories, $attributes );
|
|
||||||
$suggestions = $this->product_data_suggestion_service->get_suggestions( $product_data_request );
|
|
||||||
} catch ( Product_Data_Suggestion_Exception $e ) {
|
|
||||||
return new WP_Error( 'error', $e->getMessage(), array( 'status' => $e->getCode() ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
return rest_ensure_response( $suggestions );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
new Product_Data_Suggestion_API();
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Updating AI endpoints for product editing features.
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Completion Exception Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\Completion;
|
|
||||||
|
|
||||||
use Automattic\WooCommerce\AI\Exception\Woo_AI_Exception;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Completion Exception Class
|
|
||||||
*/
|
|
||||||
class Completion_Exception extends Woo_AI_Exception {}
|
|
|
@ -1,188 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Jetpack Completion Service Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\Completion;
|
|
||||||
|
|
||||||
use Automattic\Jetpack\Connection\Client;
|
|
||||||
use Jetpack;
|
|
||||||
use Jetpack_Options;
|
|
||||||
use JsonException;
|
|
||||||
use WP_Error;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jetpack Service class.
|
|
||||||
*/
|
|
||||||
class Jetpack_Completion_Service implements Completion_Service_Interface {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The timeout for the completion request.
|
|
||||||
*/
|
|
||||||
private const COMPLETION_TIMEOUT = 60;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the completion from the API.
|
|
||||||
*
|
|
||||||
* @param array $arguments An array of arguments to send to the API.
|
|
||||||
*
|
|
||||||
* @return string The completion response.
|
|
||||||
*
|
|
||||||
* @throws Completion_Exception If the request fails.
|
|
||||||
*/
|
|
||||||
public function get_completion( array $arguments ): string {
|
|
||||||
$this->validate_jetpack_connection();
|
|
||||||
|
|
||||||
$site_id = $this->get_site_id();
|
|
||||||
$response = $this->send_request_to_api( $site_id, $arguments );
|
|
||||||
|
|
||||||
return $this->process_response( $response );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the Jetpack connection.
|
|
||||||
*
|
|
||||||
* @throws Completion_Exception If Jetpack connection is not ready.
|
|
||||||
*/
|
|
||||||
private function validate_jetpack_connection(): void {
|
|
||||||
if ( ! $this->is_jetpack_ready() ) {
|
|
||||||
throw new Completion_Exception( __( 'Not connected to Jetpack. Please make sure that Jetpack is active and connected.', 'woocommerce' ), 400 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether Jetpack connection is ready.
|
|
||||||
*
|
|
||||||
* @return bool True if Jetpack connection is ready, false otherwise.
|
|
||||||
*/
|
|
||||||
private function is_jetpack_ready(): bool {
|
|
||||||
return Jetpack::connection()->has_connected_owner() && Jetpack::is_connection_ready();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the Jetpack Site ID.
|
|
||||||
*
|
|
||||||
* @return string The Jetpack Site ID.
|
|
||||||
*
|
|
||||||
* @throws Completion_Exception If no Jetpack Site ID is found.
|
|
||||||
*/
|
|
||||||
private function get_site_id(): string {
|
|
||||||
$site_id = Jetpack_Options::get_option( 'id' );
|
|
||||||
if ( ! $site_id ) {
|
|
||||||
throw new Completion_Exception( __( 'No Jetpack Site ID found. Please make sure that Jetpack is active and connected.', 'woocommerce' ), 400 );
|
|
||||||
}
|
|
||||||
|
|
||||||
return (string) $site_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends request to the API and gets the response.
|
|
||||||
*
|
|
||||||
* @param string $site_id The site ID.
|
|
||||||
* @param array $arguments An array of arguments to send to the API.
|
|
||||||
*
|
|
||||||
* @return array|WP_Error The response from the API.
|
|
||||||
*/
|
|
||||||
private function send_request_to_api( string $site_id, array $arguments ) {
|
|
||||||
return Client::wpcom_json_api_request_as_user(
|
|
||||||
"/sites/{$site_id}/jetpack-ai/completions",
|
|
||||||
'2',
|
|
||||||
array(
|
|
||||||
'method' => 'POST',
|
|
||||||
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
|
|
||||||
'timeout' => self::COMPLETION_TIMEOUT,
|
|
||||||
),
|
|
||||||
$arguments
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes the API response.
|
|
||||||
*
|
|
||||||
* @param array|WP_Error $response The response from the API.
|
|
||||||
*
|
|
||||||
* @return string The completion response.
|
|
||||||
*
|
|
||||||
* @throws Completion_Exception If there's an error in the response.
|
|
||||||
*/
|
|
||||||
private function process_response( $response ): string {
|
|
||||||
if ( is_wp_error( $response ) ) {
|
|
||||||
$this->handle_wp_error( $response );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! isset( $response['response']['code'] ) || 200 !== $response['response']['code'] ) {
|
|
||||||
/* translators: %s: The error message. */
|
|
||||||
throw new Completion_Exception( sprintf( __( 'Failed to get completion ( reason: %s )', 'woocommerce' ), $response['response']['message'] ), 400 );
|
|
||||||
}
|
|
||||||
|
|
||||||
$response_body = wp_remote_retrieve_body( $response );
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Extract the string from the response. Response might be wrapped in quotes and escaped. E.g. "{ \n \"foo\": \"bar\" \n }".
|
|
||||||
$decoded = json_decode( $response_body, true, 512, JSON_THROW_ON_ERROR );
|
|
||||||
} catch ( JsonException $e ) {
|
|
||||||
$this->handle_json_exception( $e );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->get_completion_string_from_decoded_response( $decoded, $response_body );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles WP_Error response.
|
|
||||||
*
|
|
||||||
* @param WP_Error $response The WP_Error response.
|
|
||||||
*
|
|
||||||
* @throws Completion_Exception With the error message from the response.
|
|
||||||
*/
|
|
||||||
private function handle_wp_error( WP_Error $response ): void {
|
|
||||||
/* translators: %s: The error message. */
|
|
||||||
throw new Completion_Exception( sprintf( __( 'Failed to get completion ( reason: %s )', 'woocommerce' ), $response->get_error_message() ), 400 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles JSON parsing exceptions.
|
|
||||||
*
|
|
||||||
* @param JsonException $e The JSON exception.
|
|
||||||
*
|
|
||||||
* @throws Completion_Exception With the error message from the JSON exception.
|
|
||||||
*/
|
|
||||||
private function handle_json_exception( JsonException $e ): void {
|
|
||||||
/* translators: %s: The error message. */
|
|
||||||
throw new Completion_Exception( sprintf( __( 'Failed to decode completion response ( reason: %s )', 'woocommerce' ), $e->getMessage() ), 500, $e );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the completion string from the decoded response.
|
|
||||||
*
|
|
||||||
* @param mixed $decoded The decoded JSON response.
|
|
||||||
* @param string $response_body The original response body.
|
|
||||||
*
|
|
||||||
* @return string The completion string.
|
|
||||||
*
|
|
||||||
* @throws Completion_Exception If the decoded response is invalid or empty.
|
|
||||||
*/
|
|
||||||
private function get_completion_string_from_decoded_response( $decoded, string $response_body ): string {
|
|
||||||
if ( ! is_string( $decoded ) ) {
|
|
||||||
// Check if the response is an error.
|
|
||||||
if ( isset( $decoded['code'] ) ) {
|
|
||||||
$error_message = $decoded['message'] ?? $decoded['code'];
|
|
||||||
/* translators: %s: The error message. */
|
|
||||||
throw new Completion_Exception( sprintf( __( 'Failed to get completion ( reason: %s )', 'woocommerce' ), $error_message ), 400 );
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the decoded response is not an error, it means that the response was not wrapped in quotes and escaped, so we can use it as is.
|
|
||||||
$decoded = $response_body;
|
|
||||||
}
|
|
||||||
if ( empty( $decoded ) || ! is_string( $decoded ) ) {
|
|
||||||
/* translators: %s: The response body. */
|
|
||||||
throw new Completion_Exception( sprintf( __( 'Invalid or empty completion response: %s', 'woocommerce' ), $response_body ), 500 );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $decoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Completion Service Interface
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\Completion;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Completion Service Interface.
|
|
||||||
*/
|
|
||||||
interface Completion_Service_Interface {
|
|
||||||
/**
|
|
||||||
* Gets the completion from the API.
|
|
||||||
*
|
|
||||||
* @param array $arguments An array of arguments to send to the API.
|
|
||||||
*
|
|
||||||
* @return string The completion response.
|
|
||||||
*
|
|
||||||
* @throws Completion_Exception If the request fails.
|
|
||||||
*/
|
|
||||||
public function get_completion( array $arguments ): string;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Woo AI Exception Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\Exception;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Woo AI Exception Class
|
|
||||||
*/
|
|
||||||
class Woo_AI_Exception extends Exception {}
|
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Product Data Suggestion Exception Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\ProductDataSuggestion;
|
|
||||||
|
|
||||||
use Automattic\WooCommerce\AI\Exception\Woo_AI_Exception;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product Data Suggestion Exception Class
|
|
||||||
*/
|
|
||||||
class Product_Data_Suggestion_Exception extends Woo_AI_Exception {}
|
|
|
@ -1,163 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Woo AI Attribute Suggestion Prompt Generator Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\ProductDataSuggestion;
|
|
||||||
|
|
||||||
use Automattic\WooCommerce\AI\PromptFormatter\Json_Request_Formatter;
|
|
||||||
use Automattic\WooCommerce\AI\PromptFormatter\Product_Attribute_Formatter;
|
|
||||||
use Automattic\WooCommerce\AI\PromptFormatter\Product_Category_Formatter;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attribute Suggestion Prompt Generator Class
|
|
||||||
*/
|
|
||||||
class Product_Data_Suggestion_Prompt_Generator {
|
|
||||||
private const PROMPT_TEMPLATE = <<<PROMPT_TEMPLATE
|
|
||||||
You are a WooCommerce SEO and marketing expert.
|
|
||||||
Using the product's name, description, tags, categories, and other attributes,
|
|
||||||
provide three optimized alternatives to the product's %s to enhance the store's SEO performance and sales.
|
|
||||||
Provide the best option for the product's %s based on the product properties.
|
|
||||||
Identify the language used in the given title and use the same language in your response.
|
|
||||||
Return only the alternative value for product's %s in the "content" part of your response.
|
|
||||||
Product titles should contain at least 20 characters.
|
|
||||||
Return a short and concise reason for each suggestion in seven words in the "reason" part of your response.
|
|
||||||
The product's properties are:
|
|
||||||
%s
|
|
||||||
PROMPT_TEMPLATE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The product category formatter.
|
|
||||||
*
|
|
||||||
* @var Product_Category_Formatter
|
|
||||||
*/
|
|
||||||
protected $product_category_formatter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The JSON request formatter.
|
|
||||||
*
|
|
||||||
* @var Json_Request_Formatter
|
|
||||||
*/
|
|
||||||
protected $json_request_formatter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The product attribute formatter.
|
|
||||||
*
|
|
||||||
* @var Product_Attribute_Formatter
|
|
||||||
*/
|
|
||||||
protected $product_attribute_formatter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product_Data_Suggestion_Prompt_Generator constructor.
|
|
||||||
*
|
|
||||||
* @param Product_Category_Formatter $product_category_formatter The product category formatter.
|
|
||||||
* @param Product_Attribute_Formatter $product_attribute_formatter The product attribute formatter.
|
|
||||||
* @param Json_Request_Formatter $json_request_formatter The JSON request formatter.
|
|
||||||
*/
|
|
||||||
public function __construct( Product_Category_Formatter $product_category_formatter, Product_Attribute_Formatter $product_attribute_formatter, Json_Request_Formatter $json_request_formatter ) {
|
|
||||||
$this->product_category_formatter = $product_category_formatter;
|
|
||||||
$this->product_attribute_formatter = $product_attribute_formatter;
|
|
||||||
$this->json_request_formatter = $json_request_formatter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the user prompt based on the request.
|
|
||||||
*
|
|
||||||
* @param Product_Data_Suggestion_Request $request The request to build the prompt for.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function get_user_prompt( Product_Data_Suggestion_Request $request ): string {
|
|
||||||
$request_prompt = $this->get_request_prompt( $request );
|
|
||||||
|
|
||||||
$prompt = sprintf(
|
|
||||||
self::PROMPT_TEMPLATE,
|
|
||||||
$request->requested_data,
|
|
||||||
$request->requested_data,
|
|
||||||
$request->requested_data,
|
|
||||||
$request_prompt
|
|
||||||
);
|
|
||||||
|
|
||||||
// Append the JSON request prompt.
|
|
||||||
$prompt .= "\n" . $this->get_example_response( $request );
|
|
||||||
|
|
||||||
return $prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a prompt for the request.
|
|
||||||
*
|
|
||||||
* @param Product_Data_Suggestion_Request $request The request to build the prompt for.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function get_request_prompt( Product_Data_Suggestion_Request $request ): string {
|
|
||||||
$request_prompt = '';
|
|
||||||
|
|
||||||
if ( ! empty( $request->name ) ) {
|
|
||||||
$request_prompt .= sprintf(
|
|
||||||
"\nName: %s",
|
|
||||||
$request->name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! empty( $request->description ) ) {
|
|
||||||
$request_prompt .= sprintf(
|
|
||||||
"\nDescription: %s",
|
|
||||||
$request->description
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! empty( $request->tags ) ) {
|
|
||||||
$request_prompt .= sprintf(
|
|
||||||
"\nTags (comma separated): %s",
|
|
||||||
implode( ', ', $request->tags )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! empty( $request->categories ) ) {
|
|
||||||
$request_prompt .= sprintf(
|
|
||||||
"\nCategories (comma separated, child categories separated with >): %s",
|
|
||||||
$this->product_category_formatter->format( $request->categories )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! empty( $request->attributes ) ) {
|
|
||||||
$request_prompt .= sprintf(
|
|
||||||
"\n%s",
|
|
||||||
$this->product_attribute_formatter->format( $request->attributes )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $request_prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an example response for the request.
|
|
||||||
*
|
|
||||||
* @param Product_Data_Suggestion_Request $request The request to build the example response for.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function get_example_response( Product_Data_Suggestion_Request $request ): string {
|
|
||||||
$response_array = array(
|
|
||||||
'suggestions' => array(
|
|
||||||
array(
|
|
||||||
'content' => sprintf( 'An improved alternative to the product\'s %s', $request->requested_data ),
|
|
||||||
'reason' => sprintf( 'First concise reason why this %s helps the SEO and sales of the product.', $request->requested_data ),
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'content' => sprintf( 'Another improved alternative to the product\'s %s', $request->requested_data ),
|
|
||||||
'reason' => sprintf( 'Second concise reason this %s helps the SEO and sales of the product.', $request->requested_data ),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->json_request_formatter->format( $response_array );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Woo AI Attribute Suggestion Request Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\ProductDataSuggestion;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attribute Suggestion Request class.
|
|
||||||
*/
|
|
||||||
class Product_Data_Suggestion_Request {
|
|
||||||
const REQUESTED_DATA_NAME = 'name';
|
|
||||||
const REQUESTED_DATA_DESCRIPTION = 'description';
|
|
||||||
const REQUESTED_DATA_TAGS = 'tags';
|
|
||||||
const REQUESTED_DATA_CATEGORIES = 'categories';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the product data that is being requested (e.g. title, description, tags, etc.).
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $requested_data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the product.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description of the product.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $description;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The product tags.
|
|
||||||
*
|
|
||||||
* @var string[]
|
|
||||||
*/
|
|
||||||
public $tags;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Categories of the product as an associative array.
|
|
||||||
*
|
|
||||||
* @var string[]
|
|
||||||
*/
|
|
||||||
public $categories;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Other attributes of the product as an associative array.
|
|
||||||
*
|
|
||||||
* @var array[] Associative array of attributes. Each attribute is an associative array with the following keys:
|
|
||||||
* - name: The name of the attribute.
|
|
||||||
* - value: The value of the attribute.
|
|
||||||
*/
|
|
||||||
public $attributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param string $requested_data The key for the product data that suggestions are being requested for.
|
|
||||||
* @param string $name The name of the product.
|
|
||||||
* @param string $description The description of the product.
|
|
||||||
* @param string[] $tags The product tags.
|
|
||||||
* @param integer[] $categories Category IDs of the product as an associative array.
|
|
||||||
* @param array[] $attributes Other attributes of the product as an associative array.
|
|
||||||
*
|
|
||||||
* @throws Product_Data_Suggestion_Exception If the requested attribute is invalid.
|
|
||||||
*/
|
|
||||||
public function __construct( string $requested_data, string $name, string $description, array $tags = array(), array $categories = array(), array $attributes = array() ) {
|
|
||||||
$this->validate_requested_data( $requested_data );
|
|
||||||
|
|
||||||
$this->requested_data = $requested_data;
|
|
||||||
$this->name = $name;
|
|
||||||
$this->description = $description;
|
|
||||||
$this->tags = $tags;
|
|
||||||
$this->categories = $categories;
|
|
||||||
$this->attributes = $attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the requested attribute.
|
|
||||||
*
|
|
||||||
* @param string $requested_data The attribute that suggestions are being requested for.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*
|
|
||||||
* @throws Product_Data_Suggestion_Exception If the requested data is invalid.
|
|
||||||
*/
|
|
||||||
private function validate_requested_data( string $requested_data ): void {
|
|
||||||
$valid_requested_data_keys = array(
|
|
||||||
self::REQUESTED_DATA_NAME,
|
|
||||||
self::REQUESTED_DATA_DESCRIPTION,
|
|
||||||
self::REQUESTED_DATA_TAGS,
|
|
||||||
self::REQUESTED_DATA_CATEGORIES,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! in_array( $requested_data, $valid_requested_data_keys, true ) ) {
|
|
||||||
throw new Product_Data_Suggestion_Exception( 'Invalid requested data.', 400 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Woo AI Attribute Suggestion Service Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\ProductDataSuggestion;
|
|
||||||
|
|
||||||
use Automattic\WooCommerce\AI\Completion\Completion_Exception;
|
|
||||||
use Automattic\WooCommerce\AI\Completion\Completion_Service_Interface;
|
|
||||||
use JsonException;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attribute Suggestion Service class.
|
|
||||||
*/
|
|
||||||
class Product_Data_Suggestion_Service {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The prompt generator.
|
|
||||||
*
|
|
||||||
* @var Product_Data_Suggestion_Prompt_Generator
|
|
||||||
*/
|
|
||||||
protected $prompt_generator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The completion service.
|
|
||||||
*
|
|
||||||
* @var Completion_Service_Interface
|
|
||||||
*/
|
|
||||||
protected $completion_service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param Product_Data_Suggestion_Prompt_Generator $prompt_generator The prompt generator.
|
|
||||||
* @param Completion_Service_Interface $completion_service The completion service.
|
|
||||||
*/
|
|
||||||
public function __construct( Product_Data_Suggestion_Prompt_Generator $prompt_generator, Completion_Service_Interface $completion_service ) {
|
|
||||||
$this->prompt_generator = $prompt_generator;
|
|
||||||
$this->completion_service = $completion_service;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get suggestions for the given request.
|
|
||||||
*
|
|
||||||
* @param Product_Data_Suggestion_Request $request The request.
|
|
||||||
*
|
|
||||||
* @return array An array of suggestions. Each suggestion is an associative array with the following keys:
|
|
||||||
* - content: The suggested content.
|
|
||||||
* - reason: The reason for the suggestion.
|
|
||||||
*
|
|
||||||
* @throws Product_Data_Suggestion_Exception If If getting the suggestions fails or the suggestions cannot be decoded from JSON.
|
|
||||||
*/
|
|
||||||
public function get_suggestions( Product_Data_Suggestion_Request $request ): array {
|
|
||||||
$arguments = array(
|
|
||||||
'content' => $this->prompt_generator->get_user_prompt( $request ),
|
|
||||||
'skip_cache' => true,
|
|
||||||
'feature' => 'woo_ai_plugin',
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$completion = $this->completion_service->get_completion( $arguments );
|
|
||||||
} catch ( Completion_Exception $e ) {
|
|
||||||
/* translators: %s: The error message. */
|
|
||||||
throw new Product_Data_Suggestion_Exception( sprintf( __( 'Failed to fetch the suggestions: %s', 'woocommerce' ), $e->getMessage() ), $e->getCode(), $e );
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return json_decode( $completion, true, 512, JSON_THROW_ON_ERROR );
|
|
||||||
} catch ( JsonException $e ) {
|
|
||||||
throw new Product_Data_Suggestion_Exception( 'Failed to decode the suggestions. Please try again.', 400, $e );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* JSON Request Formatter class.
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\PromptFormatter;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Request Formatter class.
|
|
||||||
*/
|
|
||||||
class Json_Request_Formatter implements Prompt_Formatter_Interface {
|
|
||||||
const JSON_REQUEST_PROMPT = <<<JSON_REQUEST_PROMPT
|
|
||||||
Structure your response as JSON (RFC 8259), similar to this example:
|
|
||||||
%s
|
|
||||||
|
|
||||||
You speak only JSON (RFC 8259). Output only the JSON response. Do not say anything else or use normal text.
|
|
||||||
JSON_REQUEST_PROMPT;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a prompt so that we can get a JSON response from the completion API.
|
|
||||||
*
|
|
||||||
* @param array $data An example array of data to include in the request as a possible JSON response.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*
|
|
||||||
* @throws InvalidArgumentException If the input data is not valid.
|
|
||||||
*/
|
|
||||||
public function format( $data ): string {
|
|
||||||
if ( ! $this->validate_data( $data ) ) {
|
|
||||||
throw new InvalidArgumentException( 'Invalid input data. Provide an array.' );
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf(
|
|
||||||
self::JSON_REQUEST_PROMPT,
|
|
||||||
wp_json_encode( $data )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the data to make sure it can be formatted.
|
|
||||||
*
|
|
||||||
* @param mixed $data The data to format.
|
|
||||||
*
|
|
||||||
* @return bool True if the data is valid, false otherwise.
|
|
||||||
*/
|
|
||||||
public function validate_data( $data ): bool {
|
|
||||||
return ! empty( $data ) && is_array( $data );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Product Attribute Formatter Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\PromptFormatter;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product Attribute Formatter class.
|
|
||||||
*/
|
|
||||||
class Product_Attribute_Formatter implements Prompt_Formatter_Interface {
|
|
||||||
/**
|
|
||||||
* The attribute labels.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $attribute_labels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product_Attribute_Formatter constructor.
|
|
||||||
*/
|
|
||||||
public function __construct() {
|
|
||||||
$this->attribute_labels = wc_get_attribute_taxonomy_labels();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format attributes for the prompt
|
|
||||||
*
|
|
||||||
* @param array $data An associative array of attributes with the format { "name": "name", "value": "value" }.
|
|
||||||
*
|
|
||||||
* @return string A string containing the formatted attributes.
|
|
||||||
*
|
|
||||||
* @throws InvalidArgumentException If the input data is not valid.
|
|
||||||
*/
|
|
||||||
public function format( $data ): string {
|
|
||||||
if ( ! $this->validate_data( $data ) ) {
|
|
||||||
throw new InvalidArgumentException( 'Invalid input data. Provide an array of attributes.' );
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatted_attributes = '';
|
|
||||||
|
|
||||||
foreach ( $data as $attribute ) {
|
|
||||||
// Skip if the attribute value is empty or if the attribute label is empty.
|
|
||||||
if ( empty( $attribute['value'] ) || empty( $this->attribute_labels[ $attribute['name'] ] ) ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$label = $this->attribute_labels[ $attribute['name'] ];
|
|
||||||
|
|
||||||
$formatted_attributes .= sprintf( "%s: \"%s\"\n", $label, $attribute['value'] );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $formatted_attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the data to make sure it can be formatted.
|
|
||||||
*
|
|
||||||
* @param mixed $data The data to format.
|
|
||||||
*
|
|
||||||
* @return bool True if the data is valid, false otherwise.
|
|
||||||
*/
|
|
||||||
public function validate_data( $data ): bool {
|
|
||||||
if ( empty( $data ) || ! is_array( $data ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ( $data as $attribute ) {
|
|
||||||
if ( empty( $attribute['name'] ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Product Category Formatter Class
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\PromptFormatter;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use WP_Term;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product Category Formatter class.
|
|
||||||
*/
|
|
||||||
class Product_Category_Formatter implements Prompt_Formatter_Interface {
|
|
||||||
private const CATEGORY_TAXONOMY = 'product_cat';
|
|
||||||
private const PARENT_CATEGORY_SEPARATOR = ' > ';
|
|
||||||
private const UNCATEGORIZED_SLUG = 'uncategorized';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the category names from the category ids from WooCommerce and recursively get all the parent categories and prepend them to the category names separated by >.
|
|
||||||
*
|
|
||||||
* @param array $data The category ids.
|
|
||||||
*
|
|
||||||
* @return string A string containing the formatted categories. E.g., "Books > Fiction, Books > Novels > Fiction"
|
|
||||||
*
|
|
||||||
* @throws InvalidArgumentException If the input data is not an array.
|
|
||||||
*/
|
|
||||||
public function format( $data ): string {
|
|
||||||
if ( ! $this->validate_data( $data ) ) {
|
|
||||||
throw new InvalidArgumentException( 'Invalid input data. Provide an array of category ids.' );
|
|
||||||
}
|
|
||||||
|
|
||||||
$categories = array();
|
|
||||||
foreach ( $data as $category_id ) {
|
|
||||||
$category = get_term( $category_id, self::CATEGORY_TAXONOMY );
|
|
||||||
|
|
||||||
// If the category is not found, or it is the uncategorized category, skip it.
|
|
||||||
if ( ! $category instanceof WP_Term || self::UNCATEGORIZED_SLUG === $category->slug ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$categories[] = $this->format_category_name( $category );
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode( ', ', $categories );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the data to make sure it can be formatted.
|
|
||||||
*
|
|
||||||
* @param mixed $data The data to format.
|
|
||||||
*
|
|
||||||
* @return bool True if the data is valid, false otherwise.
|
|
||||||
*/
|
|
||||||
public function validate_data( $data ): bool {
|
|
||||||
return ! empty( $data ) && is_array( $data );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get formatted category name with parent categories prepended separated by >.
|
|
||||||
*
|
|
||||||
* @param WP_Term $category The category as a WP_Term object.
|
|
||||||
*
|
|
||||||
* @return string The formatted category name.
|
|
||||||
*/
|
|
||||||
private function format_category_name( WP_Term $category ): string {
|
|
||||||
$parent_categories = $this->get_parent_categories( $category->term_id );
|
|
||||||
$parent_categories[] = $category->name;
|
|
||||||
|
|
||||||
return implode( self::PARENT_CATEGORY_SEPARATOR, $parent_categories );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get parent categories for the given category id.
|
|
||||||
*
|
|
||||||
* @param int $category_id The category id.
|
|
||||||
*
|
|
||||||
* @return array An array of names of the parent categories in the order of the hierarchy.
|
|
||||||
*/
|
|
||||||
private function get_parent_categories( int $category_id ): array {
|
|
||||||
$parent_category_ids = get_ancestors( $category_id, self::CATEGORY_TAXONOMY );
|
|
||||||
|
|
||||||
if ( empty( $parent_category_ids ) ) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$parent_category_ids = array_reverse( $parent_category_ids );
|
|
||||||
|
|
||||||
return array_map(
|
|
||||||
function ( $parent_category_id ) {
|
|
||||||
$parent_category = get_term( $parent_category_id, self::CATEGORY_TAXONOMY );
|
|
||||||
|
|
||||||
return $parent_category->name;
|
|
||||||
},
|
|
||||||
$parent_category_ids
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Prompt Formatter Interface.
|
|
||||||
*
|
|
||||||
* @package Woo_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\AI\PromptFormatter;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt Formatter Interface.
|
|
||||||
*/
|
|
||||||
interface Prompt_Formatter_Interface {
|
|
||||||
/**
|
|
||||||
* Formats the data into a prompt.
|
|
||||||
*
|
|
||||||
* @param mixed $data The data to format.
|
|
||||||
*
|
|
||||||
* @return string The formatted prompt.
|
|
||||||
*/
|
|
||||||
public function format( $data ): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the data to make sure it can be formatted.
|
|
||||||
*
|
|
||||||
* @param mixed $data The data to format.
|
|
||||||
*
|
|
||||||
* @return bool True if the data is valid, false otherwise.
|
|
||||||
*/
|
|
||||||
public function validate_data( $data ): bool;
|
|
||||||
}
|
|
|
@ -6,12 +6,12 @@ import { useRef, useState } from '@wordpress/element';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { askQuestion } from '../utils';
|
import { getCompletion } from '../utils';
|
||||||
|
|
||||||
type StopReason = 'abort' | 'finished' | 'error' | 'interrupted';
|
type StopReason = 'abort' | 'finished' | 'error' | 'interrupted';
|
||||||
|
|
||||||
type UseCompletionProps = {
|
type UseCompletionProps = {
|
||||||
onStreamMessage: ( message: string, chunk: string ) => void;
|
onStreamMessage?: ( message: string, chunk: string ) => void;
|
||||||
onCompletionFinished?: (
|
onCompletionFinished?: (
|
||||||
reason: StopReason,
|
reason: StopReason,
|
||||||
previousContent: string
|
previousContent: string
|
||||||
|
@ -20,7 +20,7 @@ type UseCompletionProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCompletion = ( {
|
export const useCompletion = ( {
|
||||||
onStreamMessage,
|
onStreamMessage = () => {},
|
||||||
onCompletionFinished = () => {},
|
onCompletionFinished = () => {},
|
||||||
onStreamError = () => {},
|
onStreamError = () => {},
|
||||||
}: UseCompletionProps ) => {
|
}: UseCompletionProps ) => {
|
||||||
|
@ -56,7 +56,7 @@ export const useCompletion = ( {
|
||||||
onStreamError( error );
|
onStreamError( error );
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestCompletion = async ( question: string ) => {
|
const requestCompletion = async ( prompt: string ) => {
|
||||||
if ( completionSource.current ) {
|
if ( completionSource.current ) {
|
||||||
stopCompletion( 'interrupted' );
|
stopCompletion( 'interrupted' );
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export const useCompletion = ( {
|
||||||
let suggestionsSource;
|
let suggestionsSource;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
suggestionsSource = await askQuestion( question );
|
suggestionsSource = await getCompletion( prompt );
|
||||||
} catch ( e ) {
|
} catch ( e ) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.debug( 'Completion connection error encountered', e );
|
console.debug( 'Completion connection error encountered', e );
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
ProductDataSuggestionRequest,
|
ProductDataSuggestionRequest,
|
||||||
ApiErrorResponse,
|
ApiErrorResponse,
|
||||||
} from '../utils/types';
|
} from '../utils/types';
|
||||||
|
import { requestJetpackToken } from '../utils';
|
||||||
|
|
||||||
type ProductDataSuggestionSuccessResponse = {
|
type ProductDataSuggestionSuccessResponse = {
|
||||||
suggestions: ProductDataSuggestion[];
|
suggestions: ProductDataSuggestion[];
|
||||||
|
@ -24,14 +25,15 @@ export const useProductDataSuggestions = () => {
|
||||||
request: ProductDataSuggestionRequest
|
request: ProductDataSuggestionRequest
|
||||||
): Promise< ProductDataSuggestion[] > => {
|
): Promise< ProductDataSuggestion[] > => {
|
||||||
try {
|
try {
|
||||||
const response =
|
const token = await requestJetpackToken();
|
||||||
|
const { suggestions } =
|
||||||
await apiFetch< ProductDataSuggestionSuccessResponse >( {
|
await apiFetch< ProductDataSuggestionSuccessResponse >( {
|
||||||
path: '/wooai/product-data-suggestions',
|
path: '/wooai/product-data-suggestions',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: request,
|
data: { ...request, token },
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return response.suggestions;
|
return suggestions;
|
||||||
} catch ( error ) {
|
} catch ( error ) {
|
||||||
/* eslint-disable-next-line no-console */
|
/* eslint-disable-next-line no-console */
|
||||||
console.error( error );
|
console.error( error );
|
||||||
|
|
|
@ -126,10 +126,10 @@ export function WriteItForMeButtonContainer() {
|
||||||
0,
|
0,
|
||||||
MAX_TITLE_LENGTH
|
MAX_TITLE_LENGTH
|
||||||
) }."`,
|
) }."`,
|
||||||
'Identify the language used in this product title and use the same language in your response.',
|
|
||||||
'Use a 9th grade reading level.',
|
'Use a 9th grade reading level.',
|
||||||
`Make the description ${ DESCRIPTION_MAX_LENGTH } words or less.`,
|
`Make the description ${ DESCRIPTION_MAX_LENGTH } words or less.`,
|
||||||
'Structure the description into paragraphs using standard HTML <p> tags.',
|
'Structure the description into paragraphs using standard HTML <p> tags.',
|
||||||
|
'Identify the language used in this product title and use the same language in your response.',
|
||||||
'Only if appropriate, use <ul> and <li> tags to list product features.',
|
'Only if appropriate, use <ul> and <li> tags to list product features.',
|
||||||
'When appropriate, use <strong> and <em> tags to emphasize text.',
|
'When appropriate, use <strong> and <em> tags to emphasize text.',
|
||||||
'Do not include a top-level heading at the beginning description.',
|
'Do not include a top-level heading at the beginning description.',
|
||||||
|
|
|
@ -10,12 +10,19 @@ import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
|
||||||
*/
|
*/
|
||||||
import MagicIcon from '../../assets/images/icons/magic.svg';
|
import MagicIcon from '../../assets/images/icons/magic.svg';
|
||||||
import AlertIcon from '../../assets/images/icons/alert.svg';
|
import AlertIcon from '../../assets/images/icons/alert.svg';
|
||||||
import { productData } from '../utils';
|
|
||||||
import { useProductDataSuggestions, useProductSlug } from '../hooks';
|
|
||||||
import {
|
import {
|
||||||
ProductDataSuggestion,
|
getProductName,
|
||||||
ProductDataSuggestionRequest,
|
getPostId,
|
||||||
} from '../utils/types';
|
getPublishingStatus,
|
||||||
|
isProductDownloadable,
|
||||||
|
isProductVirtual,
|
||||||
|
getProductType,
|
||||||
|
getCategories,
|
||||||
|
getTags,
|
||||||
|
getAttributes,
|
||||||
|
} from '../utils';
|
||||||
|
import { useCompletion, useProductSlug } from '../hooks';
|
||||||
|
import { ProductDataSuggestion } from '../utils/types';
|
||||||
import { SuggestionItem, PoweredByLink, recordNameTracks } from './index';
|
import { SuggestionItem, PoweredByLink, recordNameTracks } from './index';
|
||||||
import { RandomLoadingMessage } from '../components';
|
import { RandomLoadingMessage } from '../components';
|
||||||
|
|
||||||
|
@ -56,8 +63,35 @@ export const ProductNameSuggestions = () => {
|
||||||
const [ suggestions, setSuggestions ] = useState< ProductDataSuggestion[] >(
|
const [ suggestions, setSuggestions ] = useState< ProductDataSuggestion[] >(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const { fetchSuggestions } = useProductDataSuggestions();
|
|
||||||
const { updateProductSlug } = useProductSlug();
|
const { updateProductSlug } = useProductSlug();
|
||||||
|
const { requestCompletion } = useCompletion( {
|
||||||
|
onStreamError: ( error ) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug( 'Streaming error encountered', error );
|
||||||
|
recordNameTracks( 'stop', {
|
||||||
|
reason: 'error',
|
||||||
|
error: ( error as { message?: string } )?.message || '',
|
||||||
|
} );
|
||||||
|
setSuggestionsState( SuggestionsState.Failed );
|
||||||
|
},
|
||||||
|
onCompletionFinished: ( reason, content ) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse( content );
|
||||||
|
setSuggestions( parsed.suggestions );
|
||||||
|
setSuggestionsState( SuggestionsState.None );
|
||||||
|
|
||||||
|
recordNameTracks( 'stop', {
|
||||||
|
reason: 'finished',
|
||||||
|
suggestions: parsed.suggestions,
|
||||||
|
} );
|
||||||
|
|
||||||
|
setSuggestions( parsed.suggestions );
|
||||||
|
setIsFirstLoad( false );
|
||||||
|
} catch ( e ) {
|
||||||
|
throw new Error( 'Unable to parse suggestions' );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} );
|
||||||
const nameInputRef = useRef< HTMLInputElement >(
|
const nameInputRef = useRef< HTMLInputElement >(
|
||||||
document.querySelector( '#title' )
|
document.querySelector( '#title' )
|
||||||
);
|
);
|
||||||
|
@ -158,17 +192,12 @@ export const ProductNameSuggestions = () => {
|
||||||
updateProductName( suggestion.content );
|
updateProductName( suggestion.content );
|
||||||
setSuggestions( [] );
|
setSuggestions( [] );
|
||||||
|
|
||||||
|
const productId = getPostId();
|
||||||
|
const publishingStatus = getPublishingStatus();
|
||||||
// Update product slug if product is a draft.
|
// Update product slug if product is a draft.
|
||||||
const currentProductData = productData();
|
if ( productId !== null && publishingStatus === 'draft' ) {
|
||||||
if (
|
|
||||||
currentProductData.product_id !== null &&
|
|
||||||
currentProductData.publishing_status === 'draft'
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
updateProductSlug(
|
updateProductSlug( suggestion.content, productId );
|
||||||
suggestion.content,
|
|
||||||
currentProductData.product_id
|
|
||||||
);
|
|
||||||
} catch ( e ) {
|
} catch ( e ) {
|
||||||
// Log silently if slug update fails.
|
// Log silently if slug update fails.
|
||||||
/* eslint-disable-next-line no-console */
|
/* eslint-disable-next-line no-console */
|
||||||
|
@ -177,42 +206,60 @@ export const ProductNameSuggestions = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildPrompt = () => {
|
||||||
|
const validProductData = Object.entries( {
|
||||||
|
name: getProductName(),
|
||||||
|
tags: getTags(),
|
||||||
|
attributes: getAttributes(),
|
||||||
|
product_type: getProductType(),
|
||||||
|
is_downloadable: isProductDownloadable(),
|
||||||
|
is_virtual: isProductVirtual(),
|
||||||
|
} ).reduce( ( acc, [ key, value ] ) => {
|
||||||
|
if (
|
||||||
|
typeof value === 'boolean' ||
|
||||||
|
( value instanceof Array
|
||||||
|
? Boolean( value.length )
|
||||||
|
: Boolean( value ) )
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
acc[ key ] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} );
|
||||||
|
|
||||||
|
const instructions = [
|
||||||
|
'You are a WooCommerce SEO and marketing expert.',
|
||||||
|
"Using the product's name, description, tags, categories, and other attributes, provide three optimized alternatives to the product's title to enhance the store's SEO performance and sales.",
|
||||||
|
"Provide the best option for the product's title based on the product properties.",
|
||||||
|
'Identify the language used in the given title and use the same language in your response.',
|
||||||
|
'Return only the alternative value for product\'s title in the "content" part of your response.',
|
||||||
|
'Product titles should contain at least 20 characters.',
|
||||||
|
'Return a short and concise reason for each suggestion in seven words in the "reason" part of your response.',
|
||||||
|
"The product's properties are:",
|
||||||
|
`${ JSON.stringify( validProductData ) }`,
|
||||||
|
'Here is an example of a valid response:',
|
||||||
|
'{"suggestions": [{"content": "An improved alternative to the product\'s title", "reason": "Reason for the suggestion"}, {"content": "Another improved alternative to the product title", "reason": "Reason for this suggestion"}]}',
|
||||||
|
];
|
||||||
|
|
||||||
|
return instructions.join( '\n' );
|
||||||
|
};
|
||||||
|
|
||||||
const fetchProductSuggestions = async (
|
const fetchProductSuggestions = async (
|
||||||
event: React.MouseEvent< HTMLElement >
|
event: React.MouseEvent< HTMLElement >
|
||||||
) => {
|
) => {
|
||||||
if ( ( event.target as Element )?.closest( 'a' ) ) {
|
if ( ( event.target as Element )?.closest( 'a' ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSuggestions( [] );
|
setSuggestions( [] );
|
||||||
setSuggestionsState( SuggestionsState.Fetching );
|
setSuggestionsState( SuggestionsState.Fetching );
|
||||||
try {
|
|
||||||
const currentProductData = productData();
|
|
||||||
|
|
||||||
recordNameTracks( 'start', {
|
recordNameTracks( 'start', {
|
||||||
current_title: currentProductData.name,
|
current_title: getProductName(),
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const request: ProductDataSuggestionRequest = {
|
requestCompletion( buildPrompt() );
|
||||||
requested_data: 'name',
|
|
||||||
...currentProductData,
|
|
||||||
};
|
|
||||||
|
|
||||||
const suggestionResults = await fetchSuggestions( request );
|
|
||||||
|
|
||||||
recordNameTracks( 'stop', {
|
|
||||||
reason: 'finished',
|
|
||||||
suggestions: suggestionResults,
|
|
||||||
} );
|
|
||||||
setSuggestions( suggestionResults );
|
|
||||||
setSuggestionsState( SuggestionsState.None );
|
|
||||||
setIsFirstLoad( false );
|
|
||||||
} catch ( e ) {
|
|
||||||
recordNameTracks( 'stop', {
|
|
||||||
reason: 'error',
|
|
||||||
error: ( e as { message?: string } )?.message || '',
|
|
||||||
} );
|
|
||||||
setSuggestionsState( SuggestionsState.Failed );
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldRenderSuggestionsButton = useCallback( () => {
|
const shouldRenderSuggestionsButton = useCallback( () => {
|
||||||
|
@ -247,18 +294,16 @@ export const ProductNameSuggestions = () => {
|
||||||
) }
|
) }
|
||||||
{ productName.length < MIN_TITLE_LENGTH &&
|
{ productName.length < MIN_TITLE_LENGTH &&
|
||||||
suggestionsState === SuggestionsState.None && (
|
suggestionsState === SuggestionsState.None && (
|
||||||
<>
|
<div className="wc-product-name-suggestions__tip-message">
|
||||||
<div className="wc-product-name-suggestions__tip-message">
|
<div>
|
||||||
<div>
|
<MagicImage />
|
||||||
<MagicImage />
|
{ __(
|
||||||
{ __(
|
'Enter a few descriptive words to generate product name.',
|
||||||
'Enter a few descriptive words to generate product name.',
|
'woocommerce'
|
||||||
'woocommerce'
|
) }
|
||||||
) }
|
|
||||||
</div>
|
|
||||||
<PoweredByLink />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
<PoweredByLink />
|
||||||
|
</div>
|
||||||
) }
|
) }
|
||||||
{ suggestionsState !== SuggestionsState.Failed && (
|
{ suggestionsState !== SuggestionsState.Failed && (
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export * from './productData';
|
export * from './productData';
|
||||||
export * from './shuffleArray';
|
export * from './shuffleArray';
|
||||||
export * from './jetpack-completion';
|
export * from './text-completion';
|
||||||
export * from './recordTracksFactory';
|
export * from './recordTracksFactory';
|
||||||
export * from './get-post-id';
|
export * from './get-post-id';
|
||||||
export * from './tiny-tools';
|
export * from './tiny-tools';
|
||||||
|
|
|
@ -1,32 +1,27 @@
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { Attribute, ProductData } from './types';
|
import { Attribute } from './types';
|
||||||
import { getTinyContent, getPostId } from '.';
|
import { getTinyContent } from '.';
|
||||||
|
|
||||||
|
export const getCategories = () => {
|
||||||
|
return Array.from(
|
||||||
|
document.querySelectorAll(
|
||||||
|
'#taxonomy-product_cat input[name="tax_input[product_cat][]"]'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
( item ) =>
|
||||||
|
window.getComputedStyle( item, ':before' ).content !== 'none'
|
||||||
|
)
|
||||||
|
.map( ( item ) => item.nextSibling?.nodeValue?.trim() )
|
||||||
|
.filter( Boolean );
|
||||||
|
};
|
||||||
|
|
||||||
const isElementVisible = ( element: HTMLElement ) =>
|
const isElementVisible = ( element: HTMLElement ) =>
|
||||||
! ( window.getComputedStyle( element ).display === 'none' );
|
! ( window.getComputedStyle( element ).display === 'none' );
|
||||||
|
|
||||||
const getCategories = (): string[] => {
|
export const getTags = (): string[] => {
|
||||||
const categoryCheckboxEls: NodeListOf< HTMLInputElement > =
|
|
||||||
document.querySelectorAll(
|
|
||||||
'#taxonomy-product_cat input[name="tax_input[product_cat][]"]:checked'
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempCategories: string[] = [];
|
|
||||||
|
|
||||||
categoryCheckboxEls.forEach( ( el ) => {
|
|
||||||
if ( ! el.value.length ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tempCategories.push( el.value );
|
|
||||||
} );
|
|
||||||
|
|
||||||
return tempCategories;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTags = (): string[] => {
|
|
||||||
const tagsEl: HTMLTextAreaElement | null = document.querySelector(
|
const tagsEl: HTMLTextAreaElement | null = document.querySelector(
|
||||||
'textarea[name="tax_input[product_tag]"]'
|
'textarea[name="tax_input[product_tag]"]'
|
||||||
);
|
);
|
||||||
|
@ -36,36 +31,24 @@ const getTags = (): string[] => {
|
||||||
return tags.filter( ( tag ) => tag !== '' );
|
return tags.filter( ( tag ) => tag !== '' );
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAttributes = (): Attribute[] => {
|
export const getAttributes = (): Attribute[] => {
|
||||||
const attributeSelectEls: NodeListOf< HTMLSelectElement > =
|
const attributeContainerEls = Array.from(
|
||||||
document.querySelectorAll(
|
document.querySelectorAll( '.woocommerce_attribute_data' )
|
||||||
"#product_attributes select[name^='attribute_values']"
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const tempAttributes: Attribute[] = [];
|
return attributeContainerEls.reduce( ( acc, item ) => {
|
||||||
|
const name = (
|
||||||
attributeSelectEls.forEach( ( el: HTMLSelectElement ) => {
|
item.querySelector( 'input.attribute_name' ) as HTMLInputElement
|
||||||
const attributeName =
|
)?.value;
|
||||||
el.getAttribute( 'data-taxonomy' )?.replace( 'pa_', '' ) || '';
|
const value = item.querySelector( 'textarea' )?.textContent;
|
||||||
|
if ( name && value ) {
|
||||||
const attributeValues = Array.from( el.selectedOptions )
|
acc.push( { name, value } );
|
||||||
.map( ( option ) => option.text )
|
|
||||||
.join( ',' );
|
|
||||||
|
|
||||||
if ( ! attributeValues || ! attributeName ) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return acc;
|
||||||
tempAttributes.push( {
|
}, [] as Attribute[] );
|
||||||
name: attributeName,
|
|
||||||
value: attributeValues,
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
return tempAttributes;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDescription = (): string => {
|
export const getDescription = (): string => {
|
||||||
const isBlockEditor =
|
const isBlockEditor =
|
||||||
document.querySelectorAll( '.block-editor' ).length > 0;
|
document.querySelectorAll( '.block-editor' ).length > 0;
|
||||||
|
|
||||||
|
@ -88,37 +71,25 @@ const getDescription = (): string => {
|
||||||
)?.value;
|
)?.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProductName = (): string => {
|
export const getProductName = (): string => {
|
||||||
const productNameEl: HTMLInputElement | null =
|
const productNameEl: HTMLInputElement | null =
|
||||||
document.querySelector( '#title' );
|
document.querySelector( '#title' );
|
||||||
|
|
||||||
return productNameEl ? productNameEl.value : '';
|
return productNameEl ? productNameEl.value : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProductType = () => {
|
export const getProductType = () => {
|
||||||
const productTypeEl: HTMLInputElement | null =
|
const productTypeEl: HTMLInputElement | null =
|
||||||
document.querySelector( '#product-type' );
|
document.querySelector( '#product-type' );
|
||||||
|
|
||||||
return productTypeEl ? productTypeEl.value : '';
|
return productTypeEl ? productTypeEl.value : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const productData = (): ProductData => {
|
export const getPublishingStatus = () =>
|
||||||
return {
|
( document.querySelector( '#post_status' ) as HTMLInputElement )?.value;
|
||||||
product_id: getPostId(),
|
|
||||||
name: getProductName(),
|
export const isProductVirtual = () =>
|
||||||
categories: getCategories(),
|
( document.querySelector( '#_virtual' ) as HTMLInputElement )?.checked;
|
||||||
tags: getTags(),
|
|
||||||
attributes: getAttributes(),
|
export const isProductDownloadable = () =>
|
||||||
description: getDescription(),
|
( document.querySelector( '#_downloadable' ) as HTMLInputElement )?.checked;
|
||||||
product_type: getProductType(),
|
|
||||||
is_downloadable: (
|
|
||||||
document.querySelector( '#_downloadable' ) as HTMLInputElement
|
|
||||||
)?.checked,
|
|
||||||
is_virtual: (
|
|
||||||
document.querySelector( '#_virtual' ) as HTMLInputElement
|
|
||||||
)?.checked,
|
|
||||||
publishing_status: (
|
|
||||||
document.querySelector( '#post_status' ) as HTMLInputElement
|
|
||||||
)?.value,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -28,8 +28,7 @@ declare global {
|
||||||
*
|
*
|
||||||
* @return {Promise<{token: string, blogId: string}>} The token and the blogId
|
* @return {Promise<{token: string, blogId: string}>} The token and the blogId
|
||||||
*/
|
*/
|
||||||
async function requestToken() {
|
export async function requestJetpackToken() {
|
||||||
// Trying to pick the token from localStorage
|
|
||||||
const token = localStorage.getItem( JWT_TOKEN_ID );
|
const token = localStorage.getItem( JWT_TOKEN_ID );
|
||||||
let tokenData;
|
let tokenData;
|
||||||
|
|
||||||
|
@ -49,6 +48,7 @@ async function requestToken() {
|
||||||
|
|
||||||
const apiNonce = window.JP_CONNECTION_INITIAL_STATE?.apiNonce;
|
const apiNonce = window.JP_CONNECTION_INITIAL_STATE?.apiNonce;
|
||||||
const siteSuffix = window.JP_CONNECTION_INITIAL_STATE?.siteSuffix;
|
const siteSuffix = window.JP_CONNECTION_INITIAL_STATE?.siteSuffix;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data: { token: string; blog_id: string } = await apiFetch( {
|
const data: { token: string; blog_id: string } = await apiFetch( {
|
||||||
path: '/jetpack/v4/jetpack-ai-jwt?_cacheBuster=' + Date.now(),
|
path: '/jetpack/v4/jetpack-ai-jwt?_cacheBuster=' + Date.now(),
|
||||||
|
@ -61,18 +61,14 @@ async function requestToken() {
|
||||||
|
|
||||||
const newTokenData = {
|
const newTokenData = {
|
||||||
token: data.token,
|
token: data.token,
|
||||||
/**
|
|
||||||
* TODO: make sure we return id from the .com token acquisition endpoint too
|
|
||||||
*/
|
|
||||||
blogId: siteSuffix,
|
blogId: siteSuffix,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Let's expire the token in 2 minutes
|
* Let's expire the token in 5 minutes
|
||||||
*/
|
*/
|
||||||
expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
|
expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store the token in localStorage
|
|
||||||
debugToken( 'Storing new token' );
|
debugToken( 'Storing new token' );
|
||||||
localStorage.setItem( JWT_TOKEN_ID, JSON.stringify( newTokenData ) );
|
localStorage.setItem( JWT_TOKEN_ID, JSON.stringify( newTokenData ) );
|
||||||
|
|
||||||
|
@ -85,23 +81,18 @@ async function requestToken() {
|
||||||
/**
|
/**
|
||||||
* Leaving this here to make it easier to debug the streaming API calls for now
|
* Leaving this here to make it easier to debug the streaming API calls for now
|
||||||
*
|
*
|
||||||
* @param {string} question - The query to send to the API
|
* @param {string} prompt - The query to send to the API
|
||||||
* @param {number} postId - The post where this completion is being requested, if available
|
|
||||||
*/
|
*/
|
||||||
export async function askQuestion( question: string, postId = null ) {
|
export async function getCompletion( prompt: string ) {
|
||||||
const { token } = await requestToken();
|
const { token } = await requestJetpackToken();
|
||||||
|
|
||||||
const url = new URL(
|
const url = new URL(
|
||||||
'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-query'
|
'https://public-api.wordpress.com/wpcom/v2/text-completion/stream'
|
||||||
);
|
);
|
||||||
url.searchParams.append( 'question', question );
|
|
||||||
|
url.searchParams.append( 'prompt', prompt );
|
||||||
url.searchParams.append( 'token', token );
|
url.searchParams.append( 'token', token );
|
||||||
url.searchParams.append( 'feature', WOO_AI_PLUGIN_FEATURE_NAME );
|
url.searchParams.append( 'feature', WOO_AI_PLUGIN_FEATURE_NAME );
|
||||||
|
|
||||||
if ( postId ) {
|
return new EventSource( url.toString() );
|
||||||
url.searchParams.append( 'post_id', postId );
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = new EventSource( url.toString() );
|
|
||||||
return source;
|
|
||||||
}
|
}
|
|
@ -3,19 +3,6 @@ export type Attribute = {
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProductData = {
|
|
||||||
product_id: number | null;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
categories: string[];
|
|
||||||
tags: string[];
|
|
||||||
attributes: Attribute[];
|
|
||||||
product_type: string;
|
|
||||||
is_downloadable: boolean;
|
|
||||||
is_virtual: boolean;
|
|
||||||
publishing_status: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ProductDataSuggestion = {
|
export type ProductDataSuggestion = {
|
||||||
reason: string;
|
reason: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
|
|
@ -81,23 +81,4 @@ function _woo_ai_bootstrap(): void {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add_action(
|
|
||||||
'wp_loaded',
|
|
||||||
function () {
|
|
||||||
require 'api/api.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/exception/class-woo-ai-exception.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/completion/class-completion-exception.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/completion/interface-completion-service.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/completion/class-jetpack-completion-service.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/prompt-formatter/interface-prompt-formatter.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/prompt-formatter/class-product-category-formatter.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/prompt-formatter/class-product-attribute-formatter.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/prompt-formatter/class-json-request-formatter.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/product-data-suggestion/class-product-data-suggestion-exception.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/product-data-suggestion/class-product-data-suggestion-request.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/product-data-suggestion/class-product-data-suggestion-prompt-generator.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/includes/product-data-suggestion/class-product-data-suggestion-service.php';
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
add_action( 'plugins_loaded', '_woo_ai_bootstrap' );
|
add_action( 'plugins_loaded', '_woo_ai_bootstrap' );
|
||||||
|
|
Loading…
Reference in New Issue