From e262e0f513905fe7b0118598a1bba55d001cd8b2 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Tue, 23 Jul 2024 01:45:38 +0530 Subject: [PATCH 1/4] Add API validation step for OpenAI in Settings Closes https://github.com/Codeinwp/otter-internals/issues/191 --- inc/server/class-prompt-server.php | 101 +++++++++++++++++- src/blocks/helpers/prompt.ts | 4 +- .../components/pages/Integrations.js | 52 ++++++++- 3 files changed, 147 insertions(+), 10 deletions(-) diff --git a/inc/server/class-prompt-server.php b/inc/server/class-prompt-server.php index 25198c8d9..5c3aabda3 100644 --- a/inc/server/class-prompt-server.php +++ b/inc/server/class-prompt-server.php @@ -47,6 +47,13 @@ class Prompt_Server { */ public $timeout_transient = 'otter_prompts_timeout'; + /** + * OpenAI Endpoint. + * + * @var string + */ + private static $base_url = 'https://api.openai.com/v1/chat/completions'; + /** * Initialize the class */ @@ -62,7 +69,21 @@ public function register_routes() { register_rest_route( $namespace, - '/prompt', + '/openai/key', + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'save_api_key' ), + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ), + ) + ); + + register_rest_route( + $namespace, + '/openai/prompt', array( array( 'methods' => \WP_REST_Server::READABLE, @@ -76,7 +97,7 @@ public function register_routes() { register_rest_route( $namespace, - '/generate', + '/openai/generate', array( array( 'methods' => \WP_REST_Server::CREATABLE, @@ -89,6 +110,78 @@ public function register_routes() { ); } + /** + * Save the API key. + * + * @param \WP_REST_Request $request Request object. + * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response + */ + public function save_api_key( $request ) { + $body = $request->get_body(); + $body = json_decode( $body, true ); + + if ( ! isset( $body['api_key'] ) ) { + return new \WP_Error( 'rest_invalid_json', __( 'API key is missing.', 'otter-blocks' ), array( 'status' => 400 ) ); + } + + $api_key = sanitize_text_field( $body['api_key'] ); + + if ( empty( $api_key ) ) { + update_option( 'themeisle_open_ai_api_key', $api_key ); + return new \WP_REST_Response( array( 'message' => __( 'API key saved.', 'otter-blocks' ) ), 200 ); + } + + $response = wp_remote_post( + self::$base_url, + array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $api_key, + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( + array( + 'model' => 'gpt-3.5-turbo', + 'messages' => array( + array( + 'role' => 'system', + 'content' => 'You are a helpful assistant.', + ), + array( + 'role' => 'user', + 'content' => 'Hello!', + ), + ), + ) + ), + 'timeout' => 2 * MINUTE_IN_SECONDS, + ) + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $body = wp_remote_retrieve_body( $response ); + $body = json_decode( $body ); + + if ( isset( $body->error ) ) { + if ( isset( $body->error->message ) ) { + return new \WP_Error( isset( $body->error->code ) ? $body->error->code : 'unknown_error', $body->error->message ); + } + + return new \WP_Error( 'unknown_error', __( 'An error occurred while processing the request.', 'otter-blocks' ) ); + } + + if ( json_last_error() !== JSON_ERROR_NONE ) { + return new \WP_Error( 'rest_invalid_json', __( 'Could not parse the response from OpenAI. Try again.', 'otter-blocks' ), array( 'status' => 400 ) ); + } + + update_option( 'themeisle_open_ai_api_key', $api_key ); + + return new \WP_REST_Response( array( 'message' => __( 'API key saved.', 'otter-blocks' ) ), 200 ); + } + /** * Forward the prompt to OpenAI API. * @@ -96,8 +189,6 @@ public function register_routes() { * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response */ public function forward_prompt( $request ) { - $open_ai_endpoint = 'https://api.openai.com/v1/chat/completions'; - // Get the body from request and decode it. $body = $request->get_body(); $body = json_decode( $body, true ); @@ -117,7 +208,7 @@ function ( $key ) { $body = array_diff_key( $body, $otter_data ); $response = wp_remote_post( - $open_ai_endpoint, + self::$base_url, array( 'method' => 'POST', 'headers' => array( diff --git a/src/blocks/helpers/prompt.ts b/src/blocks/helpers/prompt.ts index 0ea046b48..692f9ed3a 100644 --- a/src/blocks/helpers/prompt.ts +++ b/src/blocks/helpers/prompt.ts @@ -123,7 +123,7 @@ function promptRequestBuilder( settings?: OpenAiSettings ) { try { const response = await apiFetch({ - path: addQueryArgs( '/otter/v1/generate', {}), + path: addQueryArgs( '/otter/v1/openai/generate', {}), method: 'POST', body: JSON.stringify({ ...( metadata ?? {}), @@ -231,7 +231,7 @@ export function parseFormPromptResponseToBlocks( promptResponse: string ) { */ export function retrieveEmbeddedPrompt( promptName ?: string ) { return apiFetch({ - path: addQueryArgs( '/otter/v1/prompt', { + path: addQueryArgs( '/otter/v1/openai/prompt', { name: promptName }), method: 'GET' diff --git a/src/dashboard/components/pages/Integrations.js b/src/dashboard/components/pages/Integrations.js index 976516746..c6953966a 100644 --- a/src/dashboard/components/pages/Integrations.js +++ b/src/dashboard/components/pages/Integrations.js @@ -3,6 +3,8 @@ */ import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; + import { BaseControl, Button, @@ -20,6 +22,8 @@ import { useState } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; + import { applyFilters } from '@wordpress/hooks'; /** @@ -53,6 +57,8 @@ const Integrations = () => { const [ stripeAPI, setStripeAPI ] = useState( '' ); const [ openAISecretKey, setOpenAISecretKey ] = useState( '' ); + const { createNotice } = useDispatch( 'core/notices' ); + let ProModules = () => { return ( { variant="secondary" isSecondary disabled={ 'saving' === status } - onClick={ () => { - window.tiTrk?.with( 'otter' ).add({ feature: 'dashboard-integration', featureComponent: 'open-ai' }); - updateOption( 'themeisle_open_ai_api_key', openAISecretKey ); + onClick={ async() => { + try { + const response = await apiFetch({ + path: 'otter/v1/openai/key', + method: 'POST', + data: { + 'api_key': openAISecretKey + } + }); + + if ( ! response.success ) { + createNotice( + 'error', + response.message ?? __( 'An unknown error occurred.', 'otter-blocks' ), + { + isDismissible: true, + type: 'snackbar' + } + ); + + return; + } + + createNotice( + 'success', + __( 'API Key saved successfully.', 'otter-blocks' ), + { + isDismissible: true, + type: 'snackbar' + } + ); + } catch ( e ) { + createNotice( + 'error', + e?.message ?? __( 'An unknown error occurred.', 'otter-blocks' ), + { + isDismissible: true, + type: 'snackbar' + } + ); + + return; + } } } > { __( 'Save', 'otter-blocks' ) } From 1bfe39b43b0bd4435119148c3c4e500c7edf3230 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Tue, 23 Jul 2024 23:39:03 +0530 Subject: [PATCH 2/4] chore: add e2e --- src/blocks/test/e2e/blocks/dashboard.spec.js | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/blocks/test/e2e/blocks/dashboard.spec.js diff --git a/src/blocks/test/e2e/blocks/dashboard.spec.js b/src/blocks/test/e2e/blocks/dashboard.spec.js new file mode 100644 index 000000000..4ec1ed54d --- /dev/null +++ b/src/blocks/test/e2e/blocks/dashboard.spec.js @@ -0,0 +1,32 @@ +/** +* WordPress dependencies +*/ +import { test, expect } from '@wordpress/e2e-test-utils-playwright'; + +test.describe( 'Dashboard', () => { + test.beforeEach( async({ admin }) => { + await admin.visitAdminPage( 'admin.php?page=otter' ); + }); + + test( 'check OpenAI API key test', async({ page }) => { + const integrationsTab = page.getByRole( 'button', { name: 'Integrations' }); + await integrationsTab.click(); + await page.waitForTimeout( 1000 ); + + const openAIAccordion = page.getByRole( 'button', { name: 'OpenAI' }); + await openAIAccordion.click(); + await page.waitForTimeout( 1000 ); + + const inputArea = page.getByPlaceholder( 'OpenAI API Key' ); + await inputArea.fill( 'test' ); + + const save = page.locator( 'div' ).filter({ hasText: /^SaveGet API Key↗More Info↗$/ }).getByRole( 'button' ); + await save.click(); + await page.waitForTimeout( 1000 ); + + const snackbar = page.getByTestId( 'snackbar' ); + + expect( await snackbar.isVisible() ).toBe( true ); + expect( await snackbar.innerText() ).toContain( 'Incorrect API key provided: test.' ); + }); +}); From a54ca745613653fb7b43b555af57a600ab285726 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 31 Jul 2024 02:35:29 +0530 Subject: [PATCH 3/4] chore: implement pr review --- inc/server/class-prompt-server.php | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/inc/server/class-prompt-server.php b/inc/server/class-prompt-server.php index 5c3aabda3..4749d51cc 100644 --- a/inc/server/class-prompt-server.php +++ b/inc/server/class-prompt-server.php @@ -52,7 +52,7 @@ class Prompt_Server { * * @var string */ - private static $base_url = 'https://api.openai.com/v1/chat/completions'; + const BASE_URL = 'https://api.openai.com/v1/chat/completions'; /** * Initialize the class @@ -120,19 +120,19 @@ public function save_api_key( $request ) { $body = $request->get_body(); $body = json_decode( $body, true ); - if ( ! isset( $body['api_key'] ) ) { + if ( ! is_array( $body ) && ! isset( $body['api_key'] ) ) { return new \WP_Error( 'rest_invalid_json', __( 'API key is missing.', 'otter-blocks' ), array( 'status' => 400 ) ); } $api_key = sanitize_text_field( $body['api_key'] ); if ( empty( $api_key ) ) { - update_option( 'themeisle_open_ai_api_key', $api_key ); + delete_option( 'themeisle_open_ai_api_key' ); return new \WP_REST_Response( array( 'message' => __( 'API key saved.', 'otter-blocks' ) ), 200 ); } $response = wp_remote_post( - self::$base_url, + self::BASE_URL, array( 'method' => 'POST', 'headers' => array( @@ -165,18 +165,14 @@ public function save_api_key( $request ) { $body = wp_remote_retrieve_body( $response ); $body = json_decode( $body ); - if ( isset( $body->error ) ) { - if ( isset( $body->error->message ) ) { - return new \WP_Error( isset( $body->error->code ) ? $body->error->code : 'unknown_error', $body->error->message ); - } - - return new \WP_Error( 'unknown_error', __( 'An error occurred while processing the request.', 'otter-blocks' ) ); - } - if ( json_last_error() !== JSON_ERROR_NONE ) { return new \WP_Error( 'rest_invalid_json', __( 'Could not parse the response from OpenAI. Try again.', 'otter-blocks' ), array( 'status' => 400 ) ); } + if ( isset( $body->error ) ) { + return isset( $body->error->message ) ? new \WP_Error( isset( $body->error->code ) ? $body->error->code : 'unknown_error', $body->error->message ) : new \WP_Error( 'unknown_error', __( 'An error occurred while processing the request.', 'otter-blocks' ) ); + } + update_option( 'themeisle_open_ai_api_key', $api_key ); return new \WP_REST_Response( array( 'message' => __( 'API key saved.', 'otter-blocks' ) ), 200 ); @@ -208,7 +204,7 @@ function ( $key ) { $body = array_diff_key( $body, $otter_data ); $response = wp_remote_post( - self::$base_url, + self::BASE_URL, array( 'method' => 'POST', 'headers' => array( From d2ed9e40b6f5700805fb653505be60f53130b6cb Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Thu, 1 Aug 2024 00:24:11 +0530 Subject: [PATCH 4/4] chore: check to confirm api test response is an obj --- inc/server/class-prompt-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/server/class-prompt-server.php b/inc/server/class-prompt-server.php index 4749d51cc..5d0f38847 100644 --- a/inc/server/class-prompt-server.php +++ b/inc/server/class-prompt-server.php @@ -165,7 +165,7 @@ public function save_api_key( $request ) { $body = wp_remote_retrieve_body( $response ); $body = json_decode( $body ); - if ( json_last_error() !== JSON_ERROR_NONE ) { + if ( json_last_error() !== JSON_ERROR_NONE && ! is_object( $body ) ) { return new \WP_Error( 'rest_invalid_json', __( 'Could not parse the response from OpenAI. Try again.', 'otter-blocks' ), array( 'status' => 400 ) ); }