Store Customization > Implement Client for requests to the GPT API (https://github.com/woocommerce/woocommerce-blocks/pull/10846)

* Add the Jetpack Connection Package as a dependency for WooCommerce Blocks.

* Introduce the new Configuration Class for registering and enabling the Jetpack connection for sites and users.

* Introduce the Connection class for making requests to the GPT AI API.

* Update the get_jwt_token method.

* Update the error messages for the get_jwt_token method.

* Update the register_site method.

* Update the Configuration class structure and add Dependency Injection.

* Update structure for the Connection Class.

* Update the return type for the get_jwt_token method.

* Update method visibility for get_site_id

* Update the name and params for methods within the Connection Class

* Add tests for the Connecction class.

* Update the Constructor for the Configuration class.
This commit is contained in:
Patricia Hillebrandt 2023-09-12 10:41:43 +02:00 committed by GitHub
parent 9db927de30
commit deb71f97fa
5 changed files with 718 additions and 3 deletions

View File

@ -16,7 +16,9 @@
"ext-hash": "*",
"ext-json": "*",
"composer/installers": "^1.7.0",
"automattic/jetpack-autoloader": "^2.9.1"
"automattic/jetpack-autoloader": "^2.11",
"automattic/jetpack-connection": "^1.57",
"automattic/jetpack-config": "^1.15"
},
"require-dev": {
"phpunit/phpunit": "9.6.10",

View File

@ -4,8 +4,108 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f236079766ae0123ae86aaf43ce16a3e",
"content-hash": "fd9d5cb53700697e14d61db1776f43d9",
"packages": [
{
"name": "automattic/jetpack-a8c-mc-stats",
"version": "v1.4.21",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-a8c-mc-stats.git",
"reference": "c6ed99f2f383b24e1b49204de2be67fe014a55b1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/c6ed99f2f383b24e1b49204de2be67fe014a55b1",
"reference": "c6ed99f2f383b24e1b49204de2be67fe014a55b1",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.8",
"yoast/phpunit-polyfills": "1.1.0"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-a8c-mc-stats",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.4.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Used to record internal usage stats for Automattic. Not visible to site owners.",
"support": {
"source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v1.4.21"
},
"time": "2023-08-23T17:56:34+00:00"
},
{
"name": "automattic/jetpack-admin-ui",
"version": "v0.2.21",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-admin-ui.git",
"reference": "ca7079662d4b8038ad3c21d4ea65ba077962d268"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/ca7079662d4b8038ad3c21d4ea65ba077962d268",
"reference": "ca7079662d4b8038ad3c21d4ea65ba077962d268",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.8",
"automattic/jetpack-logo": "^1.6.2",
"automattic/wordbless": "dev-master",
"yoast/phpunit-polyfills": "1.1.0"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-admin-ui",
"textdomain": "jetpack-admin-ui",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-admin-ui/compare/${old}...${new}"
},
"branch-alias": {
"dev-trunk": "0.2.x-dev"
},
"version-constants": {
"::PACKAGE_VERSION": "src/class-admin-menu.php"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Generic Jetpack wp-admin UI elements",
"support": {
"source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.2.21"
},
"time": "2023-08-23T17:57:08+00:00"
},
{
"name": "automattic/jetpack-autoloader",
"version": "v2.11.22",
@ -65,6 +165,315 @@
},
"time": "2023-08-23T17:57:14+00:00"
},
{
"name": "automattic/jetpack-config",
"version": "v1.15.3",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-config.git",
"reference": "e434f0261fdf6751bc0f51ff65d76539f62d0dc6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/e434f0261fdf6751bc0f51ff65d76539f62d0dc6",
"reference": "e434f0261fdf6751bc0f51ff65d76539f62d0dc6",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.5"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-config",
"textdomain": "jetpack-config",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-config/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.15.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Jetpack configuration package that initializes other packages and configures Jetpack's functionality. Can be used as a base for all variants of Jetpack package usage.",
"support": {
"source": "https://github.com/Automattic/jetpack-config/tree/v1.15.3"
},
"time": "2023-06-26T22:26:32+00:00"
},
{
"name": "automattic/jetpack-connection",
"version": "v1.57.2",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-connection.git",
"reference": "284f37b8640b96e67ba6c82ca7d37c5980166f6e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/284f37b8640b96e67ba6c82ca7d37c5980166f6e",
"reference": "284f37b8640b96e67ba6c82ca7d37c5980166f6e",
"shasum": ""
},
"require": {
"automattic/jetpack-a8c-mc-stats": "^1.4.21",
"automattic/jetpack-admin-ui": "^0.2.21",
"automattic/jetpack-constants": "^1.6.23",
"automattic/jetpack-redirect": "^1.7.26",
"automattic/jetpack-roles": "^1.4.24",
"automattic/jetpack-status": "^1.18.2"
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.8",
"automattic/wordbless": "@dev",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.1.0"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-connection",
"textdomain": "jetpack-connection",
"version-constants": {
"::PACKAGE_VERSION": "src/class-package-version.php"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-connection/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.57.x-dev"
}
},
"autoload": {
"classmap": [
"legacy",
"src/",
"src/webhooks"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Everything needed to connect to the Jetpack infrastructure",
"support": {
"source": "https://github.com/Automattic/jetpack-connection/tree/v1.57.2"
},
"time": "2023-09-04T14:41:13+00:00"
},
{
"name": "automattic/jetpack-constants",
"version": "v1.6.23",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-constants.git",
"reference": "0825fb1fa94956f26adebc01be0d716a0fd3ade0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/0825fb1fa94956f26adebc01be0d716a0fd3ade0",
"reference": "0825fb1fa94956f26adebc01be0d716a0fd3ade0",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.8",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.1.0"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-constants",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-constants/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.6.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "A wrapper for defining constants in a more testable way.",
"support": {
"source": "https://github.com/Automattic/jetpack-constants/tree/v1.6.23"
},
"time": "2023-08-23T17:56:35+00:00"
},
{
"name": "automattic/jetpack-redirect",
"version": "v1.7.26",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-redirect.git",
"reference": "c958a69a5fedfc3e95d5dd595a177e16e457e611"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/c958a69a5fedfc3e95d5dd595a177e16e457e611",
"reference": "c958a69a5fedfc3e95d5dd595a177e16e457e611",
"shasum": ""
},
"require": {
"automattic/jetpack-status": "^1.18.1"
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.8",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.1.0"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-redirect",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-redirect/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.7.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Utilities to build URLs to the jetpack.com/redirect/ service",
"support": {
"source": "https://github.com/Automattic/jetpack-redirect/tree/v1.7.26"
},
"time": "2023-08-23T17:57:15+00:00"
},
{
"name": "automattic/jetpack-roles",
"version": "v1.4.24",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-roles.git",
"reference": "7c5f339d79cba60c952715eafdebb9bcc095197f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/7c5f339d79cba60c952715eafdebb9bcc095197f",
"reference": "7c5f339d79cba60c952715eafdebb9bcc095197f",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.8",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.1.0"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-roles",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-roles/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.4.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Utilities, related with user roles and capabilities.",
"support": {
"source": "https://github.com/Automattic/jetpack-roles/tree/v1.4.24"
},
"time": "2023-08-23T17:56:38+00:00"
},
{
"name": "automattic/jetpack-status",
"version": "v1.18.2",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-status.git",
"reference": "538f247b40304f38b5e2bee98cd6c0e22e0b8953"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/538f247b40304f38b5e2bee98cd6c0e22e0b8953",
"reference": "538f247b40304f38b5e2bee98cd6c0e22e0b8953",
"shasum": ""
},
"require": {
"automattic/jetpack-constants": "^1.6.23"
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.8",
"automattic/jetpack-ip": "^0.1.5",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.1.0"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-status",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-status/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.18.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Used to retrieve information about the current status of Jetpack and the site overall.",
"support": {
"source": "https://github.com/Automattic/jetpack-status/tree/v1.18.2"
},
"time": "2023-09-04T14:40:56+00:00"
},
{
"name": "composer/installers",
"version": "v1.12.0",
@ -2887,5 +3296,5 @@
"platform-overrides": {
"php": "7.4.33"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.1.0"
}

View File

@ -0,0 +1,117 @@
<?php
namespace Automattic\WooCommerce\Blocks\AI;
use Automattic\Jetpack\Config;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Connection\Utils;
/**
* Class Configuration
*/
class Configuration {
/**
* The name of the option that stores the site owner's consent to connect to the AI API.
*
* @var string
*/
private $consent_option_name = 'woocommerce_blocks_allow_ai_connection';
/**
* The Jetpack connection manager.
*
* @var Manager
*/
private $manager;
/**
* The Jetpack configuration.
*
* @var Config
*/
private $config;
/**
* Configuration constructor.
*/
public function __construct() {
if ( ! class_exists( 'Automattic\Jetpack\Connection\Manager' ) || ! class_exists( 'Automattic\Jetpack\Config' ) ) {
return;
}
$this->manager = new Manager( 'woocommerce_blocks' );
$this->config = new Config();
}
/**
* Initialize the site and user connection and registration.
*
* @return bool|\WP_Error
*/
public function init() {
if ( ! $this->should_connect() ) {
return false;
}
$this->enable_connection_feature();
return $this->register_and_connect();
}
/**
* Verify if the site should connect to Jetpack.
*
* @return bool
*/
private function should_connect() {
$site_owner_consent = get_option( $this->consent_option_name );
return $site_owner_consent && class_exists( 'Automattic\Jetpack\Connection\Utils' ) && class_exists( 'Automattic\Jetpack\Connection\Manager' );
}
/**
* Initialize Jetpack's connection feature within the WooCommerce Blocks plugin.
*
* @return void
*/
private function enable_connection_feature() {
$this->config->ensure(
'connection',
array(
'slug' => 'woocommerce/woocommerce-blocks',
'name' => 'WooCommerce Blocks',
)
);
}
/**
* Register the site with Jetpack.
*
* @return bool|\WP_Error
*/
private function register_and_connect() {
Utils::init_default_constants();
$jetpack_id = \Jetpack_Options::get_option( 'id' );
$jetpack_public = \Jetpack_Options::get_option( 'public' );
$register = $jetpack_id && $jetpack_public ? true : $this->manager->register();
if ( true === $register && ! $this->manager->is_user_connected() ) {
$this->manager->connect_user();
return true;
}
return false;
}
/**
* Unregister the site with Jetpack.
*
* @return void
*/
private function unregister_site() {
if ( $this->manager->is_connected() ) {
$this->manager->remove_connection();
}
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace Automattic\WooCommerce\Blocks\AI;
use Automattic\Jetpack\Connection\Client;
use Jetpack_Options;
/**
* Class Connection
*/
class Connection {
/**
* The post request.
*
* @param string $token The JWT token.
* @param string $prompt The prompt to send to the API.
* @param int $timeout The timeout for the request.
*
* @return mixed
*/
public function fetch_ai_response( $token, $prompt, $timeout = 15 ) {
if ( $token instanceof \WP_Error ) {
return $token;
}
$response = wp_remote_post(
'https://public-api.wordpress.com/wpcom/v2/text-completion',
array(
'body' =>
array(
'feature' => 'woocommerce_blocks_patterns',
'prompt' => $prompt,
'token' => $token,
),
'timeout' => $timeout,
)
);
if ( is_wp_error( $response ) ) {
return new \WP_Error( $response->get_error_code(), esc_html__( 'Failed to connect with the AI endpoint: try again later.', 'woo-gutenberg-products-block' ), $response->get_error_message() );
}
$body = wp_remote_retrieve_body( $response );
return json_decode( $body, true );
}
/**
* Return the site ID.
*
* @return integer|\WP_Error The site ID or a WP_Error object.
*/
public function get_site_id() {
if ( ! class_exists( Jetpack_Options::class ) ) {
return new \WP_Error( 'site-id-error', esc_html__( 'Failed to fetch the site ID: try again later.', 'woo-gutenberg-products-block' ) );
}
$site_id = Jetpack_Options::get_option( 'id' );
if ( ! $site_id ) {
return new \WP_Error( 'site-id-error', esc_html__( 'Failed to fetch the site ID: The site is not registered.', 'woo-gutenberg-products-block' ) );
}
return $site_id;
}
/**
* Fetch the JWT token.
*
* @param integer $site_id The site ID.
*
* @return string|\WP_Error The JWT token or a WP_Error object.
*/
public function get_jwt_token( $site_id ) {
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$request = Client::wpcom_json_api_request_as_user(
sprintf( '/sites/%d/jetpack-openai-query/jwt', $site_id ),
'2',
array(
'method' => 'POST',
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
)
);
$response = json_decode( wp_remote_retrieve_body( $request ) );
if ( $response instanceof \WP_Error ) {
return new \WP_Error( $response->get_error_code(), esc_html__( 'Failed to generate the JWT token', 'woo-gutenberg-products-block' ), $response->get_error_message() );
}
if ( ! isset( $response->token ) ) {
return new \WP_Error( 'failed-to-retrieve-jwt-token', esc_html__( 'Failed to retrieve the JWT token: Try again later.', 'woo-gutenberg-products-block' ) );
}
return $response->token;
}
}

View File

@ -0,0 +1,86 @@
<?php
/**
* Unit tests for the GPT AI API Connection.
*
* @package WooCommerce\AI\Tests
*/
namespace Automattic\WooCommerce\Blocks\Tests\AI;
use Automattic\WooCommerce\Blocks\AI\Connection;
use \WP_UnitTestCase;
/**
* Class Connection_Test.
*/
class ConnectionTest extends WP_UnitTestCase {
/**
* The Connection instance.
*
* @var Connection
*/
private $connection;
/**
* Initialize the connection instance.
*
* @return void
*/
protected function setUp(): void {
parent::setUp();
$this->connection = new Connection();
}
/**
* Test get_jwt_token returns an error when the request fails.
*/
public function test_get_jwt_token_with_wp_error_in_response() {
add_filter( 'pre_http_request', array( $this, 'mock_error' ) );
wp_set_current_user( 0 );
$response = $this->connection->get_jwt_token( 1 );
$this->assertInstanceOf( \WP_Error::class, $response );
$this->assertEquals( 'Failed to retrieve the JWT token: Try again later.', $response->get_error_message() );
remove_filter( 'pre_http_request', array( $this, 'mock_error' ) );
}
/**
* Test fetch_ai_response returns an error when the request fails.
*/
public function test_post_request_with_wp_error_in_response() {
add_filter( 'pre_http_request', array( $this, 'mock_error' ) );
$response = $this->connection->fetch_ai_response( 'token_value', 'prompt_value' );
$this->assertInstanceOf( \WP_Error::class, $response );
$this->assertEquals( 'Failed to connect with the AI endpoint: try again later.', $response->get_error_message() );
remove_filter( 'pre_http_request', array( $this, 'mock_error' ) );
}
/**
* Test get_site_id returns an error when the request fails.
*/
public function test_get_site_id_with_wp_error_in_response() {
add_filter( 'pre_http_request', array( $this, 'mock_error' ) );
wp_set_current_user( 0 );
$response = $this->connection->get_site_id();
$this->assertInstanceOf( \WP_Error::class, $response );
$this->assertEquals( 'Failed to fetch the site ID: The site is not registered.', $response->get_error_message() );
remove_filter( 'pre_http_request', array( $this, 'mock_error' ) );
}
/**
* Test get_site_id returns an error when the request fails.
*/
public function mock_error() {
return new \WP_Error( 'failed_http_request', 'Error.' );
}
}