Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API validation step for OpenAI in Settings #2265

Merged
merged 5 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 92 additions & 5 deletions inc/server/class-prompt-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ class Prompt_Server {
*/
public $timeout_transient = 'otter_prompts_timeout';

/**
* OpenAI Endpoint.
*
* @var string
*/
const BASE_URL = 'https://api.openai.com/v1/chat/completions';

/**
* Initialize the class
*/
Expand All @@ -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,
Expand All @@ -76,7 +97,7 @@ public function register_routes() {

register_rest_route(
$namespace,
'/generate',
'/openai/generate',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
Expand All @@ -89,15 +110,81 @@ 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 ( ! 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 ) ) {
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,
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 );
HardeepAsrani marked this conversation as resolved.
Show resolved Hide resolved
$body = json_decode( $body );

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 ) );
}

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 );
}

/**
* Forward the prompt to OpenAI API.
*
* @param \WP_REST_Request $request Request object.
* @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 );
Expand All @@ -117,7 +204,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(
Expand Down
4 changes: 2 additions & 2 deletions src/blocks/helpers/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? {}),
Expand Down Expand Up @@ -231,7 +231,7 @@ export function parseFormPromptResponseToBlocks( promptResponse: string ) {
*/
export function retrieveEmbeddedPrompt( promptName ?: string ) {
return apiFetch<PromptServerResponse>({
path: addQueryArgs( '/otter/v1/prompt', {
path: addQueryArgs( '/otter/v1/openai/prompt', {
name: promptName
}),
method: 'GET'
Expand Down
22 changes: 22 additions & 0 deletions src/blocks/test/e2e/blocks/dashboard.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ test.describe( 'Dashboard', () => {
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.' );
});

test( 'toggle AI Block Toolbar', async({ editor, page }) => {

const toggle = page.getByLabel( 'Enable AI Block Toolbar Module' );
Expand Down
52 changes: 49 additions & 3 deletions src/dashboard/components/pages/Integrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*/
import { __ } from '@wordpress/i18n';

import apiFetch from '@wordpress/api-fetch';

import {
BaseControl,
Button,
Expand All @@ -20,6 +22,8 @@ import {
useState
} from '@wordpress/element';

import { useDispatch } from '@wordpress/data';

import { applyFilters } from '@wordpress/hooks';

/**
Expand Down Expand Up @@ -53,6 +57,8 @@ const Integrations = () => {
const [ stripeAPI, setStripeAPI ] = useState( '' );
const [ openAISecretKey, setOpenAISecretKey ] = useState( '' );

const { createNotice } = useDispatch( 'core/notices' );

let ProModules = () => {
return (
<PanelBody
Expand Down Expand Up @@ -242,9 +248,49 @@ const Integrations = () => {
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' ) }
Expand Down
Loading