[Store Customization] Create a service to get the selected vertical from the business description and the verticals (https://github.com/woocommerce/woocommerce-blocks/pull/10778)

* Add the Verticals API client

* Add tests

* Refactor error handling

* Create first version of the prompt class

* Improve Vertical selector and add tests

* Remove testing code

* Update class comment

---------

Co-authored-by: Patricia Hillebrandt <patriciahillebrandt@gmail.com>
This commit is contained in:
Alba Rincón 2023-09-11 17:24:43 +02:00 committed by GitHub
parent 75bba1d55d
commit 9d803a287a
3 changed files with 306 additions and 0 deletions

View File

@ -0,0 +1,16 @@
<?php
namespace Automattic\WooCommerce\Blocks\Verticals;
use WP_Error;
interface ChatGPTClient {
/**
* Returns a text completion from the GPT API.
*
* @param string $prompt The prompt to send to the GPT API.
*
* @return string|WP_Error The text completion, or WP_Error if the request failed.
*/
public function text_completion( string $prompt );
}

View File

@ -0,0 +1,125 @@
<?php
namespace Automattic\WooCommerce\Blocks\Verticals;
use Automattic\WooCommerce\Blocks\Verticals\Client as VerticalsAPIClient;
/**
* VerticalsSelector class.
*/
class VerticalsSelector {
public const STORE_DESCRIPTION_OPTION_KEY = 'woo_ai_describe_store_description';
/**
* The verticals API client.
*
* @var VerticalsAPIClient
*/
private $verticals_api_client;
/**
* The GPT client.
*
* @var ChatGPTClient
*/
private $chat_gpt_client;
/**
* Constructor.
*
* @param VerticalsAPIClient $verticals_api_client The verticals API client.
* @param ChatGPTClient $chat_gpt_client The ChatGPT client.
*/
public function __construct( VerticalsAPIClient $verticals_api_client, ChatGPTClient $chat_gpt_client ) {
$this->verticals_api_client = $verticals_api_client;
$this->chat_gpt_client = $chat_gpt_client;
}
/**
* Gets the vertical id that better matches the business description using the GPT API.
*
* @return string|\WP_Error The vertical id, or WP_Error if the request failed.
*/
public function get_vertical_id() {
$business_description = $this->get_business_description();
if ( empty( $business_description ) ) {
return new \WP_Error(
'empty_business_description',
__( 'The business description is empty.', 'woo-gutenberg-products-block' )
);
}
$verticals = $this->verticals_api_client->get_verticals();
if ( is_wp_error( $verticals ) ) {
return $verticals; // TODO: should wrap the error in another WP_Error???
}
$prompt = $this->build_prompt( $verticals, $business_description );
$answer = $this->chat_gpt_client->text_completion( $prompt );
if ( is_wp_error( $answer ) ) {
return $answer; // TODO: should wrap the error in another WP_Error???
}
return $this->parse_answer( $answer );
}
/**
* Get the business description from the AI settings in WooCommerce.
*
* @return string The business description.
*/
private function get_business_description(): string {
return get_option( self::STORE_DESCRIPTION_OPTION_KEY, '' );
}
/**
* Build the prompt to send to the GPT API.
*
* @param array $verticals The list of verticals.
* @param string $business_description The business description.
*
* @return string The prompt to send to the GPT API.
*/
private function build_prompt( array $verticals, string $business_description ): string {
$verticals = array_map(
function ( $vertical ) {
return "[ID=${vertical['id']}, Name=\"${vertical['name']}\"]";
},
$verticals
);
if ( empty( $verticals ) ) {
return '';
}
$verticals = implode( ', ', $verticals );
return sprintf(
'Filter the objects provided below and return the one that has a title that better matches this' .
' description of an online store with the following description: "%s". Objects: %s.' .
' The response should include exclusively the ID of the object that better matches' .
' the description in the following format: [id=selected_id]. Do not include other text or explanation.',
$business_description,
$verticals
);
}
/**
* Parse the answer from the GPT API and return the id of the selected vertical.
*
* @param string $answer The answer from the GPT API.
*
* @return string The id of the selected vertical.
*/
private function parse_answer( string $answer ): string {
$pattern = '/\[id=(\d+)]/';
if ( preg_match( $pattern, $answer, $matches ) ) {
return $matches[1];
}
return '';
}
}

View File

@ -0,0 +1,165 @@
<?php
/**
* Unit tests for the Verticals Selector class.
*
* @package WooCommerce\Verticals\Tests
*/
namespace Automattic\WooCommerce\Blocks\Tests\Verticals;
use Automattic\WooCommerce\Blocks\Verticals\ChatGPTClient;
use Automattic\WooCommerce\Blocks\Verticals\Client;
use Automattic\WooCommerce\Blocks\Verticals\VerticalsSelector;
use Mockery;
use \WP_UnitTestCase;
/**
* Class Client_Test.
*/
class VerticalsSelectorTest extends WP_UnitTestCase {
private const PROMPT = 'Filter the objects provided below and return the one that has a title that better matches this ' .
'description of an online store with the following description: "The store description.". ' .
'Objects: [ID=1, Name="Vertical 1"], [ID=2, Name="Vertical 2"]. The response should include ' .
'exclusively the ID of the object that better matches the description in the following format: ' .
'[id=selected_id]. Do not include other text or explanation.';
/**
* The Verticals Selector instance.
*
* @var VerticalsSelector $selector
*/
private VerticalsSelector $selector;
/**
* The verticals API client.
*
* @var Client $verticals_api_client
*/
private Client $verticals_api_client;
/**
* The ChatGPT client.
*
* @var ChatGPTClient $chat_gpt_client
*/
private ChatGPTClient $chat_gpt_client;
/**
* The response from the verticals API.
*
* @var array $valid_verticals_response
*/
private array $valid_verticals_response = array(
array(
'id' => 1,
'name' => 'Vertical 1',
),
array(
'id' => 2,
'name' => 'Vertical 2',
),
);
/**
* Initialize the client instance.
*
* @return void
*/
protected function setUp(): void {
parent::setUp();
$this->verticals_api_client = Mockery::mock( Client::class );
$this->chat_gpt_client = Mockery::mock( ChatGPTClient::class );
$this->selector = new VerticalsSelector( $this->verticals_api_client, $this->chat_gpt_client );
}
/**
* Test get_vertical_id returns an error when the business description is empty.
*/
public function test_get_vertical_id_returns_an_error_when_the_business_description_is_empty() {
update_option( VerticalsSelector::STORE_DESCRIPTION_OPTION_KEY, '' );
$this->verticals_api_client->shouldReceive( 'get_verticals' )->never();
$this->chat_gpt_client->shouldReceive( 'text_completion' )->never();
$response = $this->selector->get_vertical_id();
$this->assertInstanceOf( 'WP_Error', $response );
$this->assertEquals( 'empty_business_description', $response->get_error_code() );
$this->assertEquals( 'The business description is empty.', $response->get_error_message() );
}
/**
* Test get_vertical_id returns an error when the verticals API request fails.
*/
public function test_get_vertical_id_returns_an_error_when_the_verticals_api_request_fails() {
update_option( VerticalsSelector::STORE_DESCRIPTION_OPTION_KEY, 'The store description.' );
$this->verticals_api_client->shouldReceive( 'get_verticals' )
->once()
->andReturn( new \WP_Error( 'verticals_api_error', 'Request to the Verticals API failed.' ) );
$this->chat_gpt_client->shouldReceive( 'text_completion' )->never();
$response = $this->selector->get_vertical_id();
$this->assertInstanceOf( 'WP_Error', $response );
$this->assertEquals( 'verticals_api_error', $response->get_error_code() );
$this->assertEquals( 'Request to the Verticals API failed.', $response->get_error_message() );
}
/**
* Test get_vertical_id returns an error when the ChatGPT API request is fails.
*/
public function test_get_vertical_id_returns_an_error_when_the_chatgpt_api_request_fails() {
update_option( VerticalsSelector::STORE_DESCRIPTION_OPTION_KEY, 'The store description.' );
$this->verticals_api_client->shouldReceive( 'get_verticals' )
->once()
->andReturn( $this->valid_verticals_response );
$this->chat_gpt_client->shouldReceive( 'text_completion' )
->once()
->with( self::PROMPT )
->andReturn( new \WP_Error( 'chatgpt_api_error', 'Request to the ChatGPT API failed.' ) );
$response = $this->selector->get_vertical_id();
$this->assertInstanceOf( 'WP_Error', $response );
$this->assertEquals( 'chatgpt_api_error', $response->get_error_code() );
$this->assertEquals( 'Request to the ChatGPT API failed.', $response->get_error_message() );
}
/**
* Test get_vertical_id returns the vertical id when the response from the GPT API is in the expected format.
*/
public function test_get_vertical_id_returns_the_vertical_id_when_the_response_from_the_gpt_api_is_in_the_expected_format() {
update_option( VerticalsSelector::STORE_DESCRIPTION_OPTION_KEY, 'The store description.' );
$this->verticals_api_client->shouldReceive( 'get_verticals' )
->once()
->andReturn( $this->valid_verticals_response );
$this->chat_gpt_client->shouldReceive( 'text_completion' )
->once()
->with( self::PROMPT )
->andReturn( '[id=1]' );
$response = $this->selector->get_vertical_id();
$this->assertEquals( 1, $response );
}
/**
* Test get_vertical_id returns an error when the response from the GPT API is not in the expected format.
*/
public function test_get_vertical_id_returns_an_error_when_the_response_from_the_gpt_api_is_not_in_the_expected_format() {
update_option( VerticalsSelector::STORE_DESCRIPTION_OPTION_KEY, 'The store description.' );
$this->verticals_api_client->shouldReceive( 'get_verticals' )
->once()
->andReturn( $this->valid_verticals_response );
$this->chat_gpt_client->shouldReceive( 'text_completion' )
->once()
->with( self::PROMPT )
->andReturn( 'Unexpected response' );
$response = $this->selector->get_vertical_id();
$this->assertEquals( '', $response );
}
}