logo
Menu
I built a WordPress AI plugin to make authors more productive. Here's how

I built a WordPress AI plugin to make authors more productive. Here's how

Learn how to build WordPress plugin from scratch and integrate it with Amazon Bedrock to build an AI content generator plugin.

Rio Astamal
Amazon Employee
Published Apr 24, 2024
Last Modified Apr 28, 2024
WordPress powers over 40% of the entire web, making it the leading content management system (CMS). With that massive reach, I figured a plugin for WordPress would have access to a massive pool of potential users. So I decided to build an AI content generator for WordPress with Amazon Bedrock — a plugin that almost anyone creating content on WordPress could use. Here's how.
Amazon Bedrock is a fully managed service that offers a choice of high-performing foundation models (FMs) along with a broad set of capabilities that you need to build generative AI applications, simplifying development with security, privacy, and responsible AI. With single API access you can choose different models from companies like AI21 Labs, Anthropic, Cohere, Meta, Mistral AI, Stability AI, and Amazon.
This AI plugin will allow WordPress authors to seamlessly integrate AI-powered content generation directly into the editor, streamlining their content creation workflow. Authors will be able to select and configure inference parameters of various AI models. Authors just need to hit a button and the plugin will automatically update the content editor and the excerpt with contents generated by AI. With quick content generation, the plugin should help authors be more productive.
In this post I will show you to build this AI plugin. You will learn how to build a plugin for WordPress and how to use AWS SDK for PHP to call Amazon Bedrock API. After completing this post you should have a fully working WordPress AI plugin to generate contents. No prior WordPress plugin development experience needed.
To give you an overview, here is what the end result would look like.

Prerequisites

Before you dive into building the WordPress AI content generator plugin, there are a few prerequisites you'll need to have in place:
  1. AWS Account: You'll need an active Amazon Web Services (AWS) account to access Amazon Bedrock and its associated resources.
  2. Docker: The guide will utilize Docker containers to run WordPress and MySQL, so you'll need to have Docker installed on your development machine. You can skip this if you already have a working WordPress environment.
Since WordPress 5.0, Gutenberg is the default editor. To extends Gutenberg’s functionality you need to know React. Yes I hear you! But — no need to worry, I've got you covered! I promise there will be no build steps and JSX involved here, just plain vanilla Javascript you already know and love!
Note: If you’re using Windows, you can follow the guide on this post using Windows Subsystem for Linux (WSL).

Create IAM user

The plugin will use credentials associated with this IAM user to call Amazon Bedrock API. As part of best practices, you should follow the principle of "least-privilege" by only granting permissions that you need. But, for the purpose of this post I will use an AWS managed policy called “AmazonBedrockFullAccess”.
  • Log in to your AWS Management Console and navigate to the IAM service page.
  • Click on Users in the left-hand menu, then click Create user.
  • Enter wp-ai-user as User name and click Next
  • For the Permissions options, choose Attach policies directly
  • Enter “bedrock” in the Search box
  • Choose AmazonBedrockFullAccess and click Next
  • On the Review and create step click Create user
Next step is to give the newly created user access keys so it can call AWS services.
  • On the Users list click wp-ai-user.
  • Click Security credentials tab, then click Create access key button
  • For the use case, choose Local code and make sure to tick the confirmation and click Next
  • Click Create access key
Your access keys should be created. Once you have downloaded, keep them in safe place. You will need this Access key and Secret access key in the next section of this post.
This is long-term credentials with IAM user, I recommend to rotate the access key frequently to increase the security.

Activate Amazon Bedrock foundation models

To give the plugin the ability to display a list of models that the author can choose to generate content, you need to activate the foundation models from the Amazon Bedrock service. Amazon Bedrock is a region specific service, so make sure you’re in correct region. In this example I’m using US East (N. Virginia) or us-east-1.
  • Log in to your AWS Management Console and change the region to US East (N. Virginia)
  • Navigate to Amazon Bedrock service page
  • Click Model access in the left-hand menu
  • Click Manage model access button in the top right corner
  • Activate the base models that you want. In my case I activated all the base models so the WordPress administrator will have a broad of choices of models when generating content.
  • Click Save changes to apply

Prepare WordPress environment

You can skip this section if you already have a working WordPress environment.
Start by creating a root directory for this project. Let’s call it wp-ai-plugin-tutorial .
1
2
mkdir wp-ai-plugin-tutorial
cd wp-ai-plugin-tutorial
Next create a root directory for our AI plugin. Let’s call it my-ai-content-generator and put it under ./wordpress/wp-content/plugins . This directory will be mounted to the WordPress container.
1
mkdir -p wordpress/wp-content/plugins/my-ai-content-generator
Run WordPress and MySQL containers using Docker compose command. Create a docker-compose.yml file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
version: '3.9'

services:
wordpress:
image: wordpress:6-apache
ports:
- 8080:80
volumes:
- ./wordpress/wp-content/plugins/my-ai-content-generator:/var/www/html/wp-content/plugins/my-ai-content-generator
environment:
WORDPRESS_DB_HOST: mysql
WORDPRESS_DB_NAME: wp_demo
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp-demo-ai
restart: always

mysql:
image: mysql:8
volumes:
- ./mysql:/var/lib/mysql
environment:
MYSQL_DATABASE: wp_demo
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp-demo-ai
MYSQL_ROOT_PASSWORD: root-demo-ai
restart: always
Now run the containers.
1
docker compose up -d
Open your browser and go to http://localhost:8080/ where you should see the WordPress installation page. Follow the instructions to complete the installation.
WordPress installation
WordPress installation

Create main plugin file

This is the fun part - let’s start to code! At very minimum a WordPress plugin is just PHP file with a special header (comments) and some hooks to attach your functions to.
Make sure you’re in wp-content/plugins/my-ai-content-generator/ directory.
1
cd wordpress/wp-content/plugins/my-ai-content-generator
Create a new PHP file. I named it the same as the directory, my-ai-content-generator.php . Run the following command to create the file.
1
touch my-ai-content-generator.php
Use following code for my-ai-content-generator.php.
1
2
3
4
5
<?php
/**
* Plugin Name: My AI Content Generator
* Description: An AI content generator plugin powered by Amazon Bedrock.
*/
Go to the WordPress Admin dashboard and click the Plugins page from the menu in the left side. You should see the plugin appears in the list. Now click Activate to enable the plugin.
Activate plugin
Activate plugin
Right now the plugin does nothing. I will gradually add functionalities to the plugin in the next sections.

Install AWS SDK for PHP v3

To call the Amazon Bedrock API, I will use the AWS SDK for PHP v3. Install the SDK using composer. Run this command inside my-ai-content-generator/ directory.
1
2
3
4
docker run --rm --interactive --tty \
-v $(pwd):/app \
-u $(id -u):$(id -g) \
composer require aws/aws-sdk-php
Once finished you should see following new files and directory.
1
ls -1
Output:
1
2
3
4
composer.json
composer.lock
my-ai-content-generator.php
vendor

Create AWS Credentials page

To allow the plugin to call the Amazon Bedrock API, it needs to be authenticated. This page allows the administrator to enter their AWS Access key id and Secret access key. The keys are then saved to the database.
As part of best practices, you should encrypt the AWS credentials. You could use openssl_encrypt function or similar. However, for the purpose of this post, I will save it as plain text to the database.
Let's continue by modifying the file my-ai-content-generator.php as shown below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
/**
* Plugin Name: My AI Content Generator
* Description: An AI content generator plugin powered by Amazon Bedrock
*/


// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// Hooks into admin_menu to add our AI content generator settings page:
// AWS credentials page and foundation models selection page
add_action('admin_menu', 'my_ai_settings_menu');

/**
* Function to render AWS credentials page.
*
* @return void
*/

function my_ai_credentials_page() {
// Check user permissions
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

my_ai_save_credentials();

// Get the current values of the access key id from the database
// Option name is 'my_ai_credentials' and has two keys 'access_key_id' and 'secret_access_key'
// (We never display the secret access key to the user)
$credentials = get_option( 'my_ai_credentials', ['access_key_id' => ''] );
$access_key_id = $credentials['access_key_id'];

// If query string updated=true exists then display a success message
$success_message = false;
if ( isset( $_GET['updated'] ) && $_GET['updated'] === 'true' ) {
$success_message = true;
}

require __DIR__ . '/views/aws-credentials-page.php';
}

/**
* Function to save AWS credentials to the database.
*
* @return void
*/

function my_ai_save_credentials() {
// Get the submitted values from the form
$option_page = $_POST['option_page'] ?? '';
$access_key_id = $_POST['access_key_id'] ?? '';
$secret_access_key = $_POST['secret_access_key'] ?? '';

// Only proceed if option_page is my-ai-credentials-page
if ( $option_page !== 'my-ai-credentials-page' ) {
return;
}

// Save the credentials to the database
update_option('my_ai_credentials', [
'access_key_id' => $access_key_id,
'secret_access_key' => $secret_access_key,
]);
}

/**
* Function to add our AI content generator settings page to the admin menu.
*
* @return void
*/

function my_ai_settings_menu() {
// Foundation model selection page
add_menu_page(
'Foundation models', // page title
'My AI Content Generator', // menu title
'manage_options', // capability
'my-ai-models-page', // menu slug
// callback function to render the page content
function() {
return ''; // Temporary output, will be updated later
},
'dashicons-admin-generic',
);

// AWS credentials page
add_submenu_page(
'my-ai-models-page', // parent menu slug
'AWS credentials', // page title
'AWS credentials', // menu title
'manage_options', // capability
'my-ai-credentials-page', // menu slug
// callback function to render the page content
'my_ai_credentials_page',
);
}
Create a new directory views/ to store HTML pages.
1
mkdir views
Create new PHP file views/aws-credentials-page.php for displaying AWS Credentials page.
1
touch views/aws-credentials-page.php
Now use the following code for the Credentials page.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// If this is a POST request, no need to display the page and redirect using javascript
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
echo '<script>window.location = "' . admin_url('admin.php?page=my-ai-credentials-page&updated=true') . '";</script>';
return;
}

?><div class="wrap">
<h1>AWS Credentials</h1>

<?php if ($success_message): ?>
<div class="updated notice notice-success is-dismissible"><p>AWS credentials saved successfully!</p></div>
<?php endif; ?>

<p>Enter your AWS credentials to use <strong>My AI Content Generator</strong>. Make sure to follow IAM best practices such as applying <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" target="_blank">principle of least-privilege</a>.</p>

<form method="post"><?php
settings_fields('my-ai-credentials-page');
?><table class="form-table">
<tr valign="top">
<th scope="row">Access Key ID</th>
<td><input required type="text" size="30" name="access_key_id" value="<?php echo esc_attr($access_key_id); ?>" /></td>
</tr>
<tr valign="top">
<th scope="row">Secret Access Key</th>
<td><input required type="password" size="30" name="secret_access_key" value="" /></td>
</tr>
</table><?php
submit_button();
?></form>
</div>
By the end of the this step your my-ai-content-generator/ directory should look like this.
1
ls -1 *
Output:
1
2
3
4
5
6
7
8
9
10
composer.json
composer.lock
my-ai-content-generator.php

vendor:
autoload.php
...

views:
aws-credentials-page.php
Reload your WordPress Admin dashboard. You should see new menu option on the left side. Click the AWS Credentials link and it will brings you to the AWS Credentials setup page. Enter the Access key and Secret key of wp-ai-user that you created in previous steps.
AWS Credentials page
AWS Credentials page

Create models selection page

Make sure you have completed the “Activate Amazon Bedrock foundation models” step before proceeding. It’s time to use the AWS SDK for PHP v3 to call the Amazon Bedrock API. Here’s how to do it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
// Initialize Amazon Bedrock using AWS SDK for PHP
require __DIR__ . '/vendor/autoload.php';

use Aws\Bedrock\BedrockClient;
use Aws\BedrockRuntime\BedrockRuntimeClient;

$bedrock = new BedrockClient([
'region' => 'us-east-1',
'version' => 'latest',
'credentials' => [
'key' => YOUR_ACCESS_KEY_ID
'secret' => YOUR_SECRET_ACCESS_KEY
]
]);

$bedrock_runtime = new BedrockRuntimeClient([
'region' => 'us-east-1',
'version' => 'latest',
'credentials' => [
'key' => YOUR_ACCESS_KEY_ID
'secret' => YOUR_SECRET_ACCESS_KEY
]
]);
You may be wondering why there are two similar classes to call Bedrock’s API. The first one BedrockClient is used to manage the foundation models e.g list available foundation models. The other, BedrockRuntimeClient is used to run inference API to the model.
To get all the foundation models, you need to call the listFoundationModels() method.
1
$bedrock->listFoundationModels();
Now let’s modify file my-ai-content-generator.php to provide authors the ability to select models which are going to be displayed when creating a post. Replace all the contents of my-ai-content-generator.php with the one below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
<?php
/**
* Plugin Name: My AI Content Generator
* Description: An AI content generator plugin powered by Amazon Bedrock
*/


// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// Load the AWS SDK for PHP from Composer autoload
require __DIR__ . '/vendor/autoload.php';
use Aws\Bedrock\BedrockClient;
use Aws\BedrockRuntime\BedrockRuntimeClient;

// Get current AWS credentials from the database
$my_ai_credentials = get_option('my_ai_credentials', ['access_key_id' => '', 'secret_access_key' => '']);

// Initialize BedrockClient and BedrockRuntimeClient, default to us-east-1
$bedrock = new BedrockClient([
'credentials' => [
'key' => $my_ai_credentials['access_key_id'],
'secret' => $my_ai_credentials['secret_access_key'],
],
'region' => 'us-east-1',
]);

$bedrock_runtime = new BedrockRuntimeClient([
'credentials' => [
'key' => $my_ai_credentials['access_key_id'],
'secret' => $my_ai_credentials['secret_access_key'],
],
'region' => 'us-east-1',
]);

/**
* Function to return instance of BedrockClient.
*
* @return BedrockClient
*/

function my_ai_bedrock_client() {
global $bedrock;
return $bedrock;
}

/**
* Function to return instance of BedrockRuntimeClient.
*
* @return BedrockRuntimeClient
*/

function my_ai_bedrock_runtime_client() {
global $bedrock_runtime;
return $bedrock_runtime;
}

// Hooks into admin_menu to add our AI content generator settings page:
// AWS credentials page and foundation models selection page
add_action('admin_menu', 'my_ai_settings_menu');

/**
* Function to render AWS credentials page.
*
* @return void
*/

function my_ai_credentials_page() {
// Check user permissions
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

my_ai_save_credentials();

// Get the current values of the access key id from the database
// Option name is 'my_ai_credentials' and has two keys 'access_key_id' and 'secret_access_key'
// (We never display the secret access key to the user)
$credentials = get_option( 'my_ai_credentials', ['access_key_id' => ''] );
$access_key_id = $credentials['access_key_id'];

// If query string updated=true exists then display a success message
$success_message = false;
if ( isset( $_GET['updated'] ) && $_GET['updated'] === 'true' ) {
$success_message = true;
}

require __DIR__ . '/views/aws-credentials-page.php';
}

/**
* Function to save AWS credentials to the database.
*
* @return void
*/

function my_ai_save_credentials() {
// Get the submitted values from the form
$option_page = $_POST['option_page'] ?? '';
$access_key_id = $_POST['access_key_id'] ?? '';
$secret_access_key = $_POST['secret_access_key'] ?? '';

// Only proceed if option_page is my-ai-credentials-page
if ( $option_page !== 'my-ai-credentials-page' ) {
return;
}

// Save the credentials to the database
update_option('my_ai_credentials', [
'access_key_id' => $access_key_id,
'secret_access_key' => $secret_access_key,
]);
}

/**
* Function to get list of foundation models from the Bedrock and caches the result to database.
*
* The database option_name should be 'my_ai_foundation_models'. It has two keys:
* 1. 'foundation_models' - an array of foundation models
* 2. 'last_updated' - the timestamp of the last update
*
* When function is called it check the cache expiration (1 day). If it expires then
* call the Bedrock API and update the cache.
*
* Response is associative arrays with 2 keys:
* - 'error' - default to null
* - 'items' - The list of foundation models
*
* @param BedrockClient $client
* @return array - list of foundation models
*/

function my_ai_get_foundation_models($client) {
$foundation_models = get_option('my_ai_foundation_models', ['foundation_models' => [], 'last_updated' => 0]);

// Check if the cache is expired (1 day)
$now = time();
$cache_expiration = 86400; // 1 day
if ( $now - $foundation_models['last_updated'] > $cache_expiration ) {
try {
// Call the Bedrock API to get the list of foundation models
$response = $client->listFoundationModels();
} catch (Exception $e) {
// If there is an error then return an empty array
return ['error' => $e->getMessage(), 'items' => []];
}

// Update the cache
update_option('my_ai_foundation_models', [
'foundation_models' => $response['modelSummaries'],
'last_updated' => $now,
]);

// Return the list of foundation models
return $response['modelSummaries'];
}

// Return the cached list of foundation models
return $foundation_models['foundation_models'];
}

/**
* Function to render foundation model selection page.
*
* @return void
*/

function my_ai_models_page() {
// Check user permissions
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

my_ai_save_selected_foundation_models();

$bedrock = my_ai_bedrock_client();

// Get current values of the foundation models from the database
$foundation_models = my_ai_get_foundation_models($bedrock);

// Get current selected foundation models from the database
$selected_foundation_models = get_option('my_ai_selected_foundation_models', []);

// If query string updated=true exists then display a success message
$success_message = false;
if ( isset( $_GET['updated'] ) && $_GET['updated'] === 'true' ) {
$success_message = true;
}

// Link to AWS credentials page
$aws_credentials_link = admin_url('admin.php?page=my-ai-credentials-page');

require __DIR__ . '/views/foundation-models-page.php';
}

/**
* Function to save selected foundation models to the database.
*
* @return void
*/

function my_ai_save_selected_foundation_models() {
// Get the submitted values from the form
$option_page = $_POST['option_page'] ?? '';
$selected_foundation_models = $_POST['foundation_models'] ?? [];

// Only proceed if option_page is my-ai-models-page
if ( $option_page !== 'my-ai-models-page' ) {
return;
}

// Save the selected foundation models to the database
update_option('my_ai_selected_foundation_models', $selected_foundation_models);
}

/**
* Function to add our AI content generator settings page to the admin menu.
*
* @return void
*/

function my_ai_settings_menu() {
// Foundation model selection page
add_menu_page(
'Foundation models', // page title
'My AI Content Generator', // menu title
'manage_options', // capability
'my-ai-models-page', // menu slug
// callback function to render the page content
'my_ai_models_page',
'dashicons-admin-generic', // menu icon
);

// AWS credentials page
add_submenu_page(
'my-ai-models-page', // parent menu slug
'Setup AWS Credentials', // page title
'AWS Credentials', // menu title
'manage_options', // capability
'my-ai-credentials-page', // menu slug
// callback function to render the page content
'my_ai_credentials_page',
);
}
If you pay attention closely on my_ai_get_foundation_models() function, it caches the result for 1 day. This is to improves the performance of the page, so it does not need to call listFoundationModels() API each time the page is loaded.
Next is to create a view file for displaying selection of foundation models. I will name this file views/foundation-models-page.php .
1
touch views/foundation-models-page.php
Use following code for the newly created file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// If this is a POST request, no need to display the page and redirect using javascript
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
echo '<script>window.location = "' . admin_url('admin.php?page=my-ai-models-page&updated=true') . '";</script>';
return;
}

?><div class="wrap">

<h1>My AI Content Generator</h1>
<?php if ($success_message) : ?>
<div class="updated notice notice-success is-dismissible"><p>Selected models saved successfully!</p></div>
<?php endif; ?>

<p>My AI Content Generator helps you write content quickly and efficiently using AI.</p>

<h2>Select Foundation Models</h2>
<p>Please select the foundation models you want to use to generate your content. Each foundation model is trained on a specific dataset and can be used to generate content of different types and sizes. Currently the default region is set to <strong>us-east-1</strong>.</p>

<?php if (isset($foundation_models['error'])) : ?>
<p>No foundation models found. </p>
<p>Make sure your <a href="<?php echo esc_url($aws_credentials_link); ?>">AWS credentials</a> is correct and having proper permissions.</p>
<p><strong>Message</strong>:<br><i><?php echo esc_html($foundation_models['error']); ?></i></p>
<?php return; endif; ?>
<form method="post"><?php
settings_fields('my-ai-models-page');
$counter = 0;

?><table class="widefat striped">
<thead>
<tr>
<td id="cb" class="manage-column column-cb check-column"><input id="cb-select-all-1" type="checkbox">
<label for="cb-select-all-1"><span class="screen-reader-text">Select All</span></label></td>
<th>No</th>
<th>Name</th>
<th>Id</th>
<th>Provider</th>
<th>Input</th>
<th>Output</th>
</tr>
</thead>
<tbody>
<?php foreach ($foundation_models as $foundation_model) : ?>
<?php
// Only show model which outputModalities is TEXT
$is_outputmodality_text = in_array('TEXT', $foundation_model['outputModalities']);
if (! $is_outputmodality_text)
{
continue;
}

// Only show supported model, which the model id is not end with suffix any number + 'k'
$is_supported_model = ! preg_match('/\d+k$/', $foundation_model['modelId']);
if (! $is_supported_model) {
continue;
}

// Exclude model ids which not support on-demand throughput
$excluded_model_ids = ['meta.llama2-13b-v1', 'meta.llama2-70b-v1'];
if (in_array($foundation_model['modelId'], $excluded_model_ids)) {
continue;
}

// Define checked variable when current foundation_model is selected
$checked = in_array($foundation_model['modelId'], $selected_foundation_models) ? 'checked' : '';
?><tr class="iedit">
<td class="check-column" style="padding: 8px 10px"><input <?php echo $checked; ?> type="checkbox" name="foundation_models[]" value="<?php echo esc_attr($foundation_model['modelId']); ?>"></td>
<td><?php echo ++$counter; ?></td>
<td><?php echo esc_html($foundation_model['modelName']); ?></td>
<td><?php echo esc_html($foundation_model['modelId']); ?></td>
<td><?php echo esc_html($foundation_model['providerName']); ?></td>
<td><?php echo esc_html(implode(', ', $foundation_model['inputModalities'])); ?></td>
<td><?php echo esc_html(implode(', ', $foundation_model['outputModalities'])); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table><?php

submit_button(); ?>
</form>

</div>
At this stage you should have working AWS Credentials and Foundation Models Selection pages. Now my-ai-content-generator/ directory should like this:
1
ls -l *
Output:
1
2
3
4
5
6
7
8
9
10
11
composer.json
composer.lock
my-ai-content-generator.php <- modified

vendor:
autoload.php
...

views:
aws-credentials-page.php
foundation-models-page.php <- new file
Reload your WordPress Admin dashboard and click My AI Content Generator from the left side of the menu. You should see the Foundation Models Selection page. Models that have been selected on this page by the administrator will be displayed as a list in the sidebar of the WordPress block editor. Here is the screenshot of the Foundation Models Selection page.
Select foundation models page
Select foundation models page

Create REST API endpoint

This REST API endpoint will be called by the content generator from the sidebar. This is where you will run inference to the Amazon Bedrock service for the selected model. Behind the scenes it calls the InvokeModel API.
To run the inference using AWS SDK for PHP you need to call invokeModel() method from BedrockRuntimeClient object.
1
$bedrock_runtime->invokeModel($params);
Here is an example how to run inference for the Mistral 7B Instruct model from Mistral AI.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$body = [
'prompt' => 'write a joke about mathematics',
'max_tokens' => 1024,
'temperature' => 1,
'top_p' => 0.8,
'top_k' => 200,
'stop' => []
];

$model_params = [
'modelId' => 'mistral.mistral-7b-instruct-v0:2',
'contentType' => 'application/json',
'body' => json_encode($body)
];
The challenge is that each model has a different set of parameters and response. Take the model from Mistral AI as an example: the prompt needs to be wrapped inside <s>[INST]Your prompt[/INST] . So, creating a function to abstract the invoke and response retrieval would be a good move. Following are some functions to abstract those tasks:
1
2
my_ai_build_bedrock_body($model_id, $params);
my_ai_parse_bedrock_response($model_id, $response);
To make it easy for client side Javascript to parse the response, I add “application prompt” to instruct the model to wrap the response inside specified tags. The application prompt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
You are an intelligent AI assistant for writing a blog post. You are an expert to generate very long, detailed and SEO optimized article.

You must take into consideration rules below when generating article:
- The first line of your response should be the title of the blog post followed by a blank line.
- Title MUST be put within <my_ai_title></my_ai_title> tags.
- The article content MUST be put within <my_ai_content></my_ai_contents> tags.
- The summary of the content MUST be put within <my_ai_summary></my_ai_summary> tags.
- Take a look at the additional instruction inside <query></query> tags to generate the content of the article.
- Article format MUST be in HTML
- Make sure to wrap each paragraph with tag <p></p>.
- Make sure to wrap each heading with tag <h2></h2> or <h3></h3>. Depending on the heading level.
- Important: Skip the preamble from your response. NEVER generate text before the article.

Here is an example of the format:
<example>
<my_ai_title>This is example title</my_ai_title>

<my_ai_content>
...[cut]...
</my_ai_content>

<my_ai_summary>
This is example of the summary of the article.
</my_ai_summary>
</example>
To register new WordPress REST API endpoints you need to hook into rest_api_init and call the register_rest_route() function. Here’s an example:
1
add_action( 'rest_api_init', 'my_ai_register_rest_apis' );
I will use /my-ai-content-generator/v1/contents as my REST endpoint to generate the AI content. The full endpoint with the hostname should look like this:
1
http://localhost:8080/?rest_route=/my-ai-content-generator/v1/contents
If you activate pretty URLs in your WordPress configuration then you can also access the endpoint via:
1
http://localhost:8080/wp-json/my-ai-content-generator/v1/contents
This REST API can only be accessed by user who has edit_posts permissions. Now let’s create the REST API endpoint. Create a new file my-ai-rest-api.php under my-ai-content-generator/ directory.
1
touch my-ai-rest-api.php
Copy and paste the following code to your text editor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// Hooks into 'rest_api_init' to add new REST API endpoints
add_action( 'rest_api_init', 'my_ai_register_rest_apis' );

/**
* Function to register new REST API endpoints
*
* @return void
*/

function my_ai_register_rest_apis() {
// Create REST API route to geneterate AI content
register_rest_route('my-ai-content-generator/v1', '/contents', [
'methods' => 'POST',
'callback' => function($request) {
$bedrock_runtime = my_ai_bedrock_runtime_client();
$content = my_ai_generate_content($bedrock_runtime, $_POST);

return $content;
},
'permission_callback' => function() {
if (! current_user_can('edit_posts')) {
return new WP_Error('rest_forbidden', esc_html__('You do not have permission to edit posts.'), [
'status' => rest_authorization_required_code(),
]);
}

return true;
},
]);
}

/**
* Function to build parameter body which sent to Amazon Bedrock
*
* @param string $model_id
* @param array $params
* @return array
*/

function my_ai_build_bedrock_body($model_id, $params) {
$param_body = [];
switch (true) {
// Amazon Titan parameters
case strpos($model_id, 'amazon.titan') === 0:
$param_body = [
'inputText' => $params['prompt'],
'textGenerationConfig' => [
'maxTokenCount' => $params['max_tokens'] ? $params['max_tokens'] : 4096,
'temperature' => $params['temperature'] ? $params['temperature'] : 0.8,
'topP' => $params['top_p'] ? $params['top_p'] : 0.9,
'stopSequences' => []
]
];
break;

// AI21 labs Jurassic parameters
case strpos($model_id, 'ai21.j2') === 0:
$param_body = [
'prompt' => $params['prompt'],
'maxTokens' => $params['max_tokens'] ? $params['max_tokens'] : 4096,
'temperature' => $params['temperature'] ? $params['temperature'] : 0.8,
'topP' => $params['top_p'] ? $params['top_p'] : 0.9,
'stopSequences' => [],
'countPenalty' => [
'scale' => 0
],
'presencePenalty' => [
'scale' => 0
],
'frequencyPenalty' => [
'scale' => 0
],
];
break;

// Anthropic Claude parameters
case strpos($model_id, 'anthropic.claude') === 0:
$param_body = [
'anthropic_version' => 'bedrock-2023-05-31',
'max_tokens' => $params['max_tokens'] ? $params['max_tokens'] : 4096,
'temperature' => $params['temperature'] ? $params['temperature'] : 0.8,
'top_k' => $params['top_k'] ? $params['top_k'] : 200,
'top_p' => $params['top_p'] ? $params['top_p'] : 0.9,
'stop_sequences' => ["\\n\\nHuman:"],
'messages' => [
[
'role' => 'user',
'content' => [
[
'type' => 'text',
'text' => $params['prompt'],
]
]
]
]
];
break;

// Cohere Command parameters
case strpos($model_id, 'cohere.command') === 0:
$param_body = [
'prompt' => $params['prompt'],
'max_tokens' => $params['max_tokens'] ? $params['max_tokens'] : 4000,
'temperature' => $params['temperature'] ? $params['temperature'] : 0.8,
'p' => $params['top_p'] ? $params['top_p'] : 0.9,
'k' => $params['top_k'] ? $params['top_k'] : 200,
'stop_sequences' => [],
'return_likelihoods' => 'NONE',
'stream' => false
];
break;

// Meta Llama2 parameters
case strpos($model_id, 'meta.llama') === 0:
$param_body = [
'prompt' => $params['prompt'],
'max_gen_len' => $params['max_tokens'] ? $params['max_tokens'] : 2048,
'temperature' => $params['temperature'] ? $params['temperature'] : 0.8,
'top_p' => $params['top_p'] ? $params['top_p'] : 0.9
];
break;

// Mistral/Mixtral parameters
case strpos($model_id, 'mistral') === 0:
$param_body = [
'prompt' => $params['prompt'],
'max_tokens' => $params['max_tokens'] ? $params['max_tokens'] : 4096,
'temperature' => $params['temperature'] ? $params['temperature'] : 0.8,
'top_p' => $params['top_p'] ? $params['top_p'] : 0.9,
'top_k' => $params['top_k'] ? $params['top_k'] : 200,
'stop' => []
];
break;
}

return $param_body;
}

/**
* Function to parse the response of Amazon Bedrock InvokeModel()
*
* @param string $model_id - Amazon Bedrock model id
* @param array $response - Amazon Bedrock InvokeModel() response
* @return array - ['text' => '', 'error' => '']
*/

function my_ai_parse_bedrock_response($model_id, $response) {
$parsed_response = [];
switch (true) {
// Amazon Titan response
case strpos($model_id, 'amazon.titan') === 0:
$parsed_response = [
'error' => null,
'text' => $response['results'][0]['outputText']
];
break;

// AI21 labs Jurassic response
case strpos($model_id, 'ai21.j2') === 0:
$parsed_response = [
'error' => null,
'text' => $response['completions'][0]['data']['text']
];
break;

// Anthropic Claude response
case strpos($model_id, 'anthropic.claude') === 0:
$parsed_response = [
'error' => null,
'text' => $response['content'][0]['text'] ?? ''
];
break;

// Cohere Command response
case strpos($model_id, 'cohere.command') === 0:
$parsed_response = [
'error' => null,
'text' => $response['generations'][0]['text'] ?? ''
];
break;

// Meta Llama2 response
case strpos($model_id, 'meta.llama') === 0:
$parsed_response = [
'error' => null,
'text' => $response['generation'] ?? ''
];
break;

// Mistral/Mixtral response
case strpos($model_id, 'mistral') === 0:
$parsed_response = [
'error' => null,
'text' => $response['outputs'][0]['text'] ?? ''
];
break;
}

return $parsed_response;
}

/**
* Function to run inference on Amazon Bedrock
*
* @param BedrockRuntimeClient $client
* @param string $model_id
* @param array $params
* @return array
*/

function my_ai_invoke_bedrock($client, $model_id, $params) {
try {
$body = my_ai_build_bedrock_body($model_id, $params);

$invoke_params = [
'modelId' => $model_id,
'contentType' => 'application/json',
'body' => json_encode($body),
];

$response = $client->invokeModel($invoke_params);
$response_body = json_decode($response['body'], true);

// Parse the response based on model id
return my_ai_parse_bedrock_response($model_id, $response_body);
} catch (Exception $e) {
return [
'error' => $e->getMessage(),
];
}
}

/**
* Function to combine our system prompt with user prompt. Some of the models
* has different prompt format.
*
* @param string $model_id
* @param stirng $user_prompt
* @return string
*/

function my_ai_build_prompt($model_id, $user_prompt) {
$system_prompt = <<<SYSTEM_PROMPT
You are an intelligent AI assistant for writing a blog post. You are an expert to generate very long, detailed and SEO optimized article.

You must take into consideration rules below when generating article:
- The first line of your response should be the title of the blog post followed by a blank line.
- Title MUST be put within <my_ai_title></my_ai_title> tags.
- The article content MUST be put within <my_ai_content></my_ai_content> tags.
- The summary of the content MUST be put within <my_ai_summary></my_ai_summary> tags. It must be put outside <my_ai_content></my_ai_content> tags.
- Article format MUST be in HTML
- Make sure to wrap each paragraph with tag <p></p>.
- Make sure to wrap each heading with tag <h2></h2> or <h3></h3>. Depending on the heading level.
- Important: Skip the preamble from your response. NEVER generate text before the article.

Here is an example of the format:
BEGIN_EXAMPLE
<my_ai_title>This is example title</my_ai_title>

<my_ai_content>
<p>This is example of opening paragraph 1.</p>
<p>This is example of opening paragraph 2.</p>

<h2>Sub heading 1</h2>
<p>This is example paragraph 1</p>
<p>This is example paragraph 2</p>
<p>This is example paragraph 3</p>

<h2>Sub heading 2</h2>
<p>This is example paragraph 1</p>
<p>This is example paragraph 2</p>
<p>This is example paragraph 3</p>
<p>This is example paragraph 4</p>
<p>This is example paragraph 5</p>

<h2>Sub heading 3</h2>
<p>This is example paragraph 1</p>
<p>This is example paragraph 2</p>
<p>This is example paragraph 3</p>
<p>This is example paragraph 4</p>

<h2>Sub heading 4</h2>
<p>This is example paragraph 1</p>
<p>This is example paragraph 2</p>
<p>This is example paragraph 3</p>

<h2>Sub heading for conclusion</h2>
<p>This is example conclusion paragraph 1</p>
<p>This is example conclusion paragraph 2</p>
</my_ai_content>
<!-- this is important </my_ai_content> should exists -->

<my_ai_summary>
This is example of the summary of the article.
</my_ai_summary>
END_EXAMPLE
SYSTEM_PROMPT
;

// Add prefix or suffix to the prompt based on the value of model id
if (strpos($model_id, 'anthropic.claude-v2') === 0) {
$final_prompt = <<<FINAL_PROMPT

Human: $system_prompt
$user_prompt

Assistant:
FINAL_PROMPT
;
}

if (strpos($model_id, 'meta.llama2') === 0) {
return <<<FINAL_PROMPT
<s>[INST]<<SYS>>$system_prompt<</SYS>>
$user_prompt
[/INST]
FINAL_PROMPT
;
}

if (strpos($model_id, 'meta.llama3') === 0) {
return <<<FINAL_PROMPT
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
$system_prompt<|eot_id|><|start_header_id|>user<|end_header_id|>
$user_prompt<|eot_id|><|start_header_id|>assistant<|end_header_id|>
FINAL_PROMPT
;
}

if (strpos($model_id, 'mistral') === 0) {
return <<<FINAL_PROMPT
<s>[INST]$system_prompt

$user_prompt
[/INST]
FINAL_PROMPT
;
}

return $system_prompt . "\n\n" . $user_prompt;
}

/**
* Function to generate AI content in JSON format.
*
* @param BedrockRuntimeClient $client
* @param array $params
* @return string
*/

function my_ai_generate_content($client, $params) {
$model_id = $params['model_id'];

// Build the prompt based on the model id
$prompt = my_ai_build_prompt($model_id, $params['prompt']);
$params['prompt'] = $prompt;

// Make sure to convert numerical parameters from string to integer/float
$params['max_tokens'] = intval($params['max_tokens']);
$params['temperature'] = floatval($params['temperature']);
$params['top_p'] = floatval($params['top_p']);
$params['top_k'] = intval($params['top_k']);

// Invoke the model
$response = my_ai_invoke_bedrock($client, $model_id, $params);

return json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
}
Modify the main plugin file my-ai-content-generator.php to include my-ai-rest-api.php. Put following code at the end of my-ai-content-generator.php.
1
require __DIR__ . '/my-ai-rest-api.php';
Now my-ai-content-generator/ should like the following:
1
ls -1 *
Output:
1
2
3
4
5
6
7
8
9
10
11
12
composer.json
composer.lock
my-ai-content-generator.php <- modified
my-ai-rest-api.php <- new file

vendor:
autoload.php
...

views:
aws-credentials-page.php
foundation-models-page.php

Create content generator sidebar

The content generator sidebar allows authors to generate content using the AI model of their choice. Authors are able to tune the inference parameters such as temperature, Top P, Top K, and maximum tokens for the output. This sidebar will be displayed when authors edit a post or a page.
To hook into Gutenberg (the default editor for WordPress) you need to create a React element. As I promised earlier no need to worry. I will not using neither any build steps like webpack nor JSX. WordPress exposes React in the global context window.React so you can access this object anywhere in your Javascript code.
WordPress itself creates a Javascript object in window.wp which holds many client side functionalities. In this case I am interested in window.wp.plugins and the method registerPlugin() . It allows registering the content generator sidebar. Here is an example:
1
2
3
4
5
6
7
8
9
10
window.wp.plugins.registerPlugin('my-ai-content-generator', {
render: () => {
// Create a new PluginSidebar element
var sidebarElement = React.createElement(window.wp.editPost.PluginSidebar, {
name: 'my-ai-sidebar-element',
title: 'My AI Content Generator',
icon: 'vault',
}, childElement);
}
});
The sidebar that holds model selection and its parameters will appear at the right side of the block editor when editing a post or a page. To hook into the sidebar block editor, a custom React element PluginSidebar is used.
To call the AI content generator REST endpoint /my-ai-content-generator/v1/contents, I will use the fetch() API. Fetch API should be available in most modern browsers both on mobile and desktop. I am using standard form data content type application/x-www-form-urlencoded for the POST body.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var response = await fetch(myAiApiUrl.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
foundation_model: foundationModel,
prompt: params.prompt,
temperature: params.temperature,
top_p: params.top_p,
top_k: params.top_k || null,
max_tokens: params.max_tokens
}),
credentials: 'include'
});
One of the most important thing to do when calling a WordPress REST endpoint is to include the nonce in the request via query string, body or HTTP header. You can get the nonce by accessing global object wpApiSettings provided by WordPress before making the request:
1
wpApiSettings.nonce
After receiving the content from the API you need to dispatch it to the editor. To update Gutenberg contents I will use two Block Editor data modules: core/block-editor for updating the contents and core/editor for updating title and the excerpt.
1
2
3
4
5
6
7
8
// Update the title in the editor
window.wp.data.dispatch('core/editor').editPost({ title: title });

// Reset the Gutenberg content and pass our content as the replacement
window.wp.data.dispatch('core/block-editor').resetBlocks( window.wp.blocks.parse( content ));

// Update the excerpt in the sidebar
window.wp.data.dispatch('core/editor').editPost({ excerpt: summary });
Create a new Javascript file my-ai-sidebar.js in my-ai-content-generator/js/ directory.
1
2
mkdir js
touch js/my-ai-sidebar.js
Use the following code for my-ai-sidebar.js.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
function my_ai_sidebar_init() {
var wpBaseUrl = window.location.href.split('/wp-admin/')[0];
// myAiSelectedFoundationModels Javascript variable are injected via WordPress hooks 'enqueue_block_editor_assets'
var foundationModels = myAiSelectedFoundationModels;

var generate_my_ai_content = async (foundationModel, params) => {
var queryStringRestRoute = new URLSearchParams({
rest_route: '/my-ai-content-generator/v1/contents',
_wpnonce: wpApiSettings.nonce
});
var myAiApiUrl = wpBaseUrl + '/?' + queryStringRestRoute;

// Call AI content generator REST endpoint
var response = await fetch(myAiApiUrl.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
model_id: foundationModel,
prompt: params.prompt,
temperature: params.temperature,
top_p: params.top_p,
top_k: params.top_k || null,
max_tokens: params.max_tokens
}),
credentials: 'include'
});

var data = await response.json();

// If the data is still in string, we need to convert it to JSON
if (typeof data === 'string') {
try {
var json = JSON.parse(data);
return json;
} catch (e) {}
}

return data;
};

// Object to store foundation model family configuration such as:
// temperature, top_p, top_k and max_tokens.
// Each model family has default value and maximum value of the configuration.
// As an example: 'amazon.titan' family
var foundationModelConfig = {
'amazon.titan': {
temperature: { min: 0, max: 1, default: 0.9 },
top_p: { min: 0, max: 1, default: 1 },
top_k: null,
max_tokens: { min: 0, max: 4096, default: 2048 }
},

'ai21.j2': {
temperature: { min: 0, max: 1, default: 0.7 },
top_p: { min: 0, max: 1, default: 1 },
top_k: null,
max_tokens: { min: 0, max: 8191, default: 2048 }
},

'anthropic.claude': {
temperature: { min: 0, max: 1, default: 1 },
top_p: { min: 0, max: 1, default: 0.99 },
top_k: { min: 0, max: 500, default: 200 },
max_tokens: { min: 0, max: 4096, default: 2048 }
},

'cohere.command': {
temperature: { min: 0, max: 1, default: 0.75 },
top_p: { min: 0, max: 1, default: 0 },
top_k: { min: 0, max: 500, default: 200 },
max_tokens: { min: 0, max: 4000, default: 2048 }
},

'meta.llama': {
temperature: { min: 0, max: 1, default: 0.5 },
top_p: { min: 0, max: 1, default: 0.9 },
top_k: null,
max_tokens: { min: 0, max: 2048, default: 2048 }
},

'mistral': {
temperature: { min: 0, max: 1, default: 0.5 },
top_p: { min: 0, max: 1, default: 0.9 },
top_k: { min: 0, max: 200, default: 200 },
max_tokens: { min: 0, max: 8192, default: 2048 }
}
};

/**
* Get the current short model id name from the selected model
* @param {string} foundationModel
* @returns {string}
*/

var getModelFamily = (foundationModel) => {
// Get the current short model id name from the selected model
var modelId = '';
for (var modelFamily of Object.keys(foundationModelConfig)) {
if (foundationModel.indexOf(modelFamily) > -1) {
modelId = modelFamily;
break;
}
}

return modelId;
}

// Initial short model name used by the foundationModelConfig
var modelFamily = getModelFamily(foundationModels[0]);

// Register the plugin elements into the Gutenberg sidebar
window.wp.plugins.registerPlugin("my-ai-sidebar", {
render: () => {
var [currentModelState, setCurrentModelState] = React.useState(foundationModels[0]);
var [promptState, setPromptState] = React.useState('');
var [temperatureState, setTemperatureState] = React.useState(foundationModelConfig[modelFamily].temperature);
var [topPState, setTopPState] = React.useState(foundationModelConfig[modelFamily].top_p);
var [topKState, setTopKState] = React.useState(foundationModelConfig[modelFamily].top_k);
var [maxTokensState, setMaxTokensState] = React.useState(foundationModelConfig[modelFamily].max_tokens);
var [buttonEnabledState, setButtonEnabledState] = React.useState(false);
var [generatingState, setGeneratingState] = React.useState(false);

// Create array of options element based on foundation models list
var optionsElement = foundationModels.map(modelId => {
return React.createElement('option', { value: modelId }, modelId);
});
var selectElement = React.createElement('select', {
id: 'my_ai_model_id', style: { marginBottom: '10px', display: 'block', width: '95%' },
value: currentModelState,
onChange: (e) => {
// Get the selected foundation model
var foundationModel = e.target.value;
console.log('FM -> ', foundationModel);

var modelId = getModelFamily(foundationModel);
setTemperatureState(foundationModelConfig[modelId].temperature);
setTopPState(foundationModelConfig[modelId].top_p);
setTopKState(foundationModelConfig[modelId].top_k);
setMaxTokensState(foundationModelConfig[modelId].max_tokens);
setCurrentModelState(foundationModel);
}
}, optionsElement);
var labelSelectModelElement = React.createElement('label', { display: 'block' }, 'Select foundation model:');
var labelPromptElement = React.createElement('label', { display: 'block' }, 'Input prompt:');
var inputPromptElement = React.createElement('textarea', {
id: 'my_ai_prompt', style: { marginBottom: '10px', display: 'block', width: '95%', height: '150px' },
placeholder: 'Write an article about the benefits of meditation',
value: promptState,
onChange: (e) => {
// Enable the generate button if the prompt is not empty
if (e.target.value.trim().length === 0) {
setButtonEnabledState(false);
return;
}

setPromptState(e.target.value);
setButtonEnabledState(true);
}
});

var buttonElement = React.createElement('button', {
id: 'my_ai_btn_generate', display: 'block', className: 'components-button is-primary',
disabled: !buttonEnabledState,
onClick: async (e) => {
var foundationModelId = document.getElementById('my_ai_model_id').value;
var prompt = document.getElementById('my_ai_prompt').value;

setGeneratingState(true)

// When the button clicked the label should change to "Generating...", once finished
// it should back to "Generate"
e.target.innerText = 'Generating...';
e.target.disabled = true;

// Call generate_my_ai_content to fetch the generated content via API
// Construct the foundation model parameters to send to the API
var modelParams = {
prompt: prompt,
temperature: temperatureState.default,
top_p: topPState.default,
top_k: topKState ? topKState.default : null,
max_tokens: maxTokensState.default
}

var response = await generate_my_ai_content(foundationModelId, modelParams);
console.log(response);

// The response contains two properties 'error' and 'text'
if (response.error) {
setGeneratingState(false);
alert(response.error);
e.target.innerText = 'Generate';
e.target.disabled = false;
document.getElementById('my_ai_prompt').focus();

return;
}

// If there is no <my_ai_title>, </my_ai_title>, <my_ai_content>, and <my_ai_content> tag
// in the response, then dispatch everything to the block editor.
// Otherwise, extract the title and content from the response.text
var validFormat = response.text.indexOf('<my_ai_title>') !== -1 &&
response.text.indexOf('</my_ai_title>') !== -1 &&
response.text.indexOf('<my_ai_content>') !== -1 &&
response.text.indexOf('</my_ai_content>') !== -1;

if (! validFormat) {
window.wp.data.dispatch('core/editor').editPost({ title: '[Unknown]' });
window.wp.data.dispatch('core/block-editor').resetBlocks( window.wp.blocks.parse( response.text ));

e.target.innerText = 'Generate';
e.target.disabled = false;
document.getElementById('my_ai_prompt').focus();

setGeneratingState(false);

return;
}

// Extract the title from the response.text using substring.
// The title inside the <my_ai_title>THE_TITLE</my_ai_title>
var title = response.text.substring(response.text.indexOf('<my_ai_title>') + '<my_ai_title>'.length, response.text.indexOf('</my_ai_title>'));
title = title.trim();

// Extract the content from the response.text using substring.
// The content inside the <my_ai_content>THE_CONTENT</my_ai_content>
var content = response.text.substring(response.text.indexOf('<my_ai_content>') + '<my_ai_content>'.length, response.text.indexOf('</my_ai_content>'));
content = content.trim();

// Dispatch the title into Gutenberg using wp.data.dispatch('core/editor').editPost()
window.wp.data.dispatch('core/editor').editPost({ title: title });

// Reset the Gutenberg content and pass our content as the replacement
window.wp.data.dispatch('core/block-editor').resetBlocks( window.wp.blocks.parse( content ));

// If the response.text has the summary then dispatch the core/editor excerpt
if (response.text.indexOf('<my_ai_summary>') !== -1) {
var summary = response.text.substring(response.text.indexOf('<my_ai_summary>') + '<my_ai_summary>'.length, response.text.indexOf('</my_ai_summary>'));
summary = summary.trim();

window.wp.data.dispatch('core/editor').editPost({ excerpt: summary });
}

e.target.innerText = 'Generate';
e.target.disabled = false;
document.getElementById('my_ai_prompt').focus();

setGeneratingState(false);
}
}, 'Generate');

var spanTemperatureElement = React.createElement('span', {
id: 'my_ai_temp_span', style: { fontWeight: 'bold' },
}, temperatureState.default);
var labelTemperatureElement = React.createElement('label', { display: 'block' },
'Temperature: ', spanTemperatureElement);
var inputTemperatureElement = React.createElement('input', {
id: 'my_ai_temp', type: 'range',
min: temperatureState.min,
max: temperatureState.max,
step: '0.1',
value: temperatureState.default,
style: { marginBottom: '10px', display: 'block', width: '95%' },
onChange: (e) => {
setTemperatureState({
min: temperatureState.min,
max: temperatureState.max,
default: e.target.value
});
}
});

var spanTopPElement = React.createElement('span', {
id: 'my_ai_top_p_span', style: { fontWeight: 'bold' },
}, topPState.default);
var labelTopPElement = React.createElement('label', { display: 'block' },
'Top P: ', spanTopPElement);
var inputTopPElement = React.createElement('input', {
id: 'my_ai_top_p', type: 'range',
min: topPState.min,
max: topPState.max,
step: '0.1',
value: topPState.default,
style: { marginBottom: '10px', display: 'block', width: '95%' },
onChange: (e) => {
setTopPState({
min: topPState.min,
max: topPState.max,
default: e.target.value
});
}
});

var spanTopKElement = React.createElement('span', {
id: 'my_ai_top_k_span', style: { fontWeight: 'bold', color: topKState ? 'inherit' : 'red' },
}, topKState ? topKState.default : 0);
var labelTopKElement = React.createElement('label', {
style: { 'display': topKState ? 'inline' : 'none' }
}, 'Top K: ', spanTopKElement);
var inputTopKElement = React.createElement('input', {
id: 'my_ai_top_k', type: 'range',
min: topKState ? topKState.min : null,
max: topKState ? topKState.max : null,
step: 1,
value: topKState ? topKState.default : 0,
style: { marginBottom: '10px', display: topKState ? 'block' : 'none' , width: '95%' },
onChange: (e) => {
if (topKState) {
setTopKState({
min: topKState.min,
max: topKState.max,
default: e.target.value
});
}
}
});

var spanMaxTokensElement = React.createElement('span', {
id: 'my_ai_max_tokens_span', style: { fontWeight: 'bold' },
}, maxTokensState.default);
var labelMaxTokensElement = React.createElement('label', { display: 'block' },
'Max Tokens: ', spanMaxTokensElement);
var inputMaxTokensElement = React.createElement('input', {
id: 'my_ai_max_tokens', type: 'range',
min: maxTokensState.min,
max: maxTokensState.max,
step: 1,
value: maxTokensState.default,
style: { marginBottom: '10px', display: 'block', width: '95%' },
onChange: (e) => {
setMaxTokensState({
min: maxTokensState.min,
max: maxTokensState.max,
default: e.target.value
});
}
});

// Array of model config elements (temperature, top p, top k, and max tokens)
var modelConfigElements = [
labelTemperatureElement, inputTemperatureElement,
labelTopPElement, inputTopPElement,
labelTopKElement, inputTopKElement,
labelMaxTokensElement, inputMaxTokensElement
];

var modelConfigElement = React.createElement('div', {
id: 'my_ai_model_config', display: 'block', style: { marginBottom: '10px' }
}, ...modelConfigElements)

var myAiElement = React.createElement('div', {
style: { paddingLeft: '16px', paddingRight: '16px', marginTop: '20px' },
id: 'my_ai_elements_container'
},
labelSelectModelElement,
selectElement,
labelPromptElement,
inputPromptElement,
modelConfigElement,
buttonElement
); // myAiElement

var pluginInfo = React.createElement(window.wp.editPost.PluginSidebar, {
name: 'my-ai-sidebar-element',
title: 'My AI Content Generator',
icon: 'welcome-write-blog'
}, myAiElement);

return pluginInfo;
}
});
}

my_ai_sidebar_init();
Now I need to load this script when an author edits a post or a page. To do so I need to hook into WordPress enqueue_block_editor_assets. Modify my-ai-content-generator.php and add following code at the end of the file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
add_action('enqueue_block_editor_assets', function() {
// Script dependencies
$dependencies = ['react', 'wp-blocks', 'wp-editor'];

// URL to js/my-ai-sidebar.js
$script_url = plugin_dir_url(__FILE__) . 'js/my-ai-sidebar.js';

// Enqueue script and use version 1.0.0 as a cache buster
wp_enqueue_script('my-ai-sidebar', $script_url, $dependencies, '1.0.0', true);

// Get current selected foundation models from the database
$selected_foundation_models = get_option('my_ai_selected_foundation_models', []);

// Add inline script so wp-ai-sidebar.js can set the selected foundation models
$javascript_line = sprintf('var myAiSelectedFoundationModels = %s;', json_encode($selected_foundation_models));
wp_add_inline_script('my-ai-sidebar', $javascript_line, 'before');
});
At this stage your my-ai-content-generator directory should look like following:
1
ls -1 *
Output:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
composer.json
composer.lock
my-ai-content-generator.php <- modified
my-ai-rest-api.php

js:
my-ai-sidebar.js <- new file

vendor:
autoload.php
...

views:
aws-credentials-page.php
foundation-models-page.php

Testing the plugin

Let’s see our plugin in action. Create a new blog post by clicking Post then Add New Post. It will open the WordPress default editor, Gutenberg. Take a look at the top right corner on your screen you should see a small pencil icon. See following image.
Sidebar icon
Sidebar icon
Click the icon and it will display My AI Content Generator sidebar. To start generating an article from AI, here are the steps:
  1. Select the model, e.g anthropic.claude-3-haiku-[version]
  2. Enter your input prompt, e.g “Write an extensive, optimized SEO article with a minimum of 2,000 words about starting a career as a web developer. Provide a long overview of the benefits of being a web developer (include stats if possible) and the challenges of being a web developer. Provide pro-tips on how to become a web developer and which programming language to choose as a beginner.”
  3. Set the temperature to 0.8, Top P to 0.9, Top K to 200 and Max tokens to 2048
  4. Click Generate
Wait for couple of moments and the editor will be automatically filled by the AI generated content. Nice! The plugin works as expected. You may try playing around with the parameters or try another model like mistral.mistral-large-[version] and click Generate button. You should have a different result each time.
My AI Content Generator plugin in action
My AI Content Generator plugin in action

Future improvements

There are a few things could be improved from the plugin:
Stream the response. Currently the content from the API is returned as single response. To make it more interactive and add an “instantaneous” feel, you could stream the response. On Amazon Bedrock you can use InvokeModelWithResponseStream API.
Generate image. It would be great if the plugin was able to provide the author with an automatically generated featured image. The featured image could be based on the article summary or different input prompt for the image. You could use SDXL 1.0 model from Stability AI or Titan Image Generator model from Amazon.
AWS Credentials. The value of secret key that stored in the database should be encrypted. In this post I save it as a plain text. This may raise a concern if your database is compromised.

Cost to run the plugin

With Amazon Bedrock, you will be charged for model inference and customization. There are two pricing plans for inference: On-Demand and Provisioned Throughput. In this post I use On-Demand. For inference you will be charged input tokens and output tokens. The price is vary between models and region.
As an example, Claude 3 Haiku model in US East (N. Virginia) region cost $0.00025 per 1,000 input tokens and $0.00125 per 1,000 output tokens. In this case the prompt that plugin sent are considered input tokens. The generated content that you received from the model are output tokens.
Keep in mind that, at the time of this writing, only Amazon Titan family models can be paid with AWS credits. Standard AWS credits cannot be used for 3rd party model providers currently.
You can read more on Amazon Bedrock pricing documentation.

Clean up

You may clean up resources created in this post using AWS Management Console or via AWS CLI for the following resources:
  • IAM user wp-ai-user
  • Deactivate Amazon Bedrock foundation models you don't need

Summary

In this post, you learn how to build WordPress plugin from scratch to generate content from AI. The content generation are powered by Amazon Bedrock. To integrate with Amazon Bedrock I use AWS SDK for PHP v3. This post demonstrate that you can use PHP to build a generative AI application.
If you have any feedbacks or questions, drop your comment below.
 

Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.

4 Comments