Skip to content

Commit 96eb8df

Browse files
jeffpauljamesmorrisondkottermindctrlswissspidy
authored
Merge pull request #96 from jamesmorrison/feature/11-excerpt-generation
Added Excerpt generation core functionality (no UI) Unlinked contributors: nholzmann. Co-authored-by: jamesmorrison <[email protected]> Co-authored-by: dkotter <[email protected]> Co-authored-by: mindctrl <[email protected]> Co-authored-by: swissspidy <[email protected]> Co-authored-by: Ref34t <[email protected]> Co-authored-by: karmatosed <[email protected]>
2 parents 3173ef0 + 5e4a608 commit 96eb8df

File tree

14 files changed

+1085
-38
lines changed

14 files changed

+1085
-38
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,6 @@
8686
"format": "Automatically fix coding standards issues",
8787
"lint": "Check coding standards compliance",
8888
"test": "Run PHPUnit tests",
89-
"stan": "Run static analysis with PHPStan"
89+
"phpstan": "Run static analysis with PHPStan"
9090
}
9191
}

docs/TESTING_REST_API.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Testing REST API Endpoints
2+
3+
This document provides instructions for testing the AI plugin's REST API endpoints using curl.
4+
5+
## Prerequisites
6+
7+
- WordPress site running (e.g., `https://wordpress-ai.test`)
8+
- Administrator account credentials
9+
- curl installed
10+
11+
## Authentication Methods
12+
13+
### Method 1: Application Password (Recommended)
14+
15+
1. **Create an Application Password:**
16+
- Log into WordPress admin
17+
- Go to Users → Your Profile
18+
- Scroll to "Application Passwords"
19+
- Create a new application password (name it "Testing" or similar)
20+
- Copy the generated password (you'll only see it once)
21+
22+
2. **Make the request:**
23+
24+
```bash
25+
curl -X POST "https://wordpress-ai.test/wp-json/wp-abilities/v1/abilities/ai/excerpt-generation/run" \
26+
-u "your-username:application-password" \
27+
-H "Content-Type: application/json" \
28+
-d '{
29+
"input": {
30+
"content": "This is a sample article about artificial intelligence and machine learning. AI has revolutionized many industries including healthcare, finance, and transportation. Machine learning algorithms can now process vast amounts of data to identify patterns and make predictions that were previously impossible."
31+
}
32+
}'
33+
```
34+
35+
### Method 2: Cookie Authentication with Nonce
36+
37+
1. **Get a nonce:**
38+
- Log into WordPress admin in your browser
39+
- Open browser DevTools → Console
40+
- Run: `wpApiSettings.nonce` (if available)
41+
42+
2. **Get your session cookie:**
43+
- Open DevTools → Application/Storage → Cookies
44+
- Copy the `wordpress_logged_in_[hash]` cookie value
45+
46+
3. **Make the request:**
47+
48+
```bash
49+
curl -X POST "https://wordpress-ai.test/wp-json/wp-abilities/v1/abilities/ai/excerpt-generation/run" \
50+
-H "Content-Type: application/json" \
51+
-H "X-WP-Nonce: YOUR_NONCE_HERE" \
52+
-H "Cookie: wordpress_logged_in_[hash]=YOUR_COOKIE_VALUE_HERE" \
53+
-d '{
54+
"input": {
55+
"content": "This is a sample article about artificial intelligence and machine learning. AI has revolutionized many industries including healthcare, finance, and transportation. Machine learning algorithms can now process vast amounts of data to identify patterns and make predictions that were previously impossible."
56+
}
57+
}'
58+
```
59+
60+
## Example: Excerpt Generation
61+
62+
**Endpoint:** `POST /wp-json/wp-abilities/v1/abilities/ai/excerpt-generation/run`
63+
64+
**Request Body:**
65+
```json
66+
{
67+
"input": {
68+
"content": "This is a sample article about artificial intelligence and machine learning. AI has revolutionized many industries including healthcare, finance, and transportation. Machine learning algorithms can now process vast amounts of data to identify patterns and make predictions that were previously impossible."
69+
}
70+
}
71+
```
72+
73+
**Full curl command (using Application Password):**
74+
```bash
75+
curl -X POST "https://wordpress-ai.test/wp-json/wp-abilities/v1/abilities/ai/excerpt-generation/run" \
76+
-u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
77+
-H "Content-Type: application/json" \
78+
-d '{
79+
"input": {
80+
"content": "This is a sample article about artificial intelligence and machine learning. AI has revolutionized many industries including healthcare, finance, and transportation. Machine learning algorithms can now process vast amounts of data to identify patterns and make predictions that were previously impossible."
81+
}
82+
}'
83+
```
84+
85+
**Expected Response:**
86+
```json
87+
{
88+
"excerpts": [
89+
"This article explores how artificial intelligence and machine learning have transformed industries like healthcare, finance, and transportation. These technologies enable algorithms to analyze massive datasets, uncovering patterns and generating predictions that were once beyond human capability.",
90+
"...",
91+
"..."
92+
]
93+
}
94+
```
95+
96+
## Example: Title Generation
97+
98+
**Endpoint:** `POST /wp-json/wp-abilities/v1/abilities/ai/title-generation/run`
99+
100+
**Request Body:**
101+
```json
102+
{
103+
"input": {
104+
"content": "Your article content here...",
105+
"candidates": 3
106+
}
107+
}
108+
```
109+
110+
**Full curl command:**
111+
```bash
112+
curl -X POST "https://wordpress-ai.test/wp-json/wp-abilities/v1/abilities/ai/title-generation/run" \
113+
-u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
114+
-H "Content-Type: application/json" \
115+
-d '{
116+
"input": {
117+
"content": "Your article content here...",
118+
"candidates": 3
119+
}
120+
}'
121+
```
122+
123+
## Common Errors
124+
125+
### 401 Unauthorized
126+
- **Cause:** Invalid or missing authentication
127+
- **Solution:** Verify your Application Password or nonce/cookie are correct
128+
129+
### 405 Method Not Allowed
130+
- **Cause:** Using GET instead of POST
131+
- **Solution:** Ensure you're using `-X POST` in curl
132+
133+
### 400 Bad Request
134+
- **Cause:** Invalid input format
135+
- **Solution:** Ensure the request body has `"input"` as the top-level key, and the JSON is valid
136+
137+
### 404 Not Found
138+
- **Cause:** Ability not registered
139+
- **Solution:** Ensure the plugin is activated and the ability is registered
140+
141+
## Testing with Postman
142+
143+
1. **Create a new POST request**
144+
2. **URL:** `https://wordpress-ai.test/wp-json/wp-abilities/v1/abilities/ai/excerpt-generation/run`
145+
3. **Authorization:**
146+
- Type: Basic Auth
147+
- Username: Your WordPress username
148+
- Password: Application Password (not your regular password)
149+
4. **Headers:**
150+
- `Content-Type: application/json`
151+
5. **Body:**
152+
- Select "raw" and "JSON"
153+
- Paste the JSON with `input` wrapper
154+
155+
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<?php
2+
/**
3+
* Excerpt generation WordPress Ability implementation.
4+
*
5+
* @package WordPress\AI
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace WordPress\AI\Abilities\Excerpt_Generation;
11+
12+
use WP_Error;
13+
use WordPress\AI\Abstracts\Abstract_Ability;
14+
use WordPress\AI_Client\AI_Client;
15+
16+
use function WordPress\AI\get_post_context;
17+
use function WordPress\AI\get_preferred_models;
18+
use function WordPress\AI\normalize_content;
19+
20+
/**
21+
* Excerpt generation WordPress Ability.
22+
*
23+
* @since x.x.x
24+
*/
25+
class Excerpt_Generation extends Abstract_Ability {
26+
27+
/**
28+
* {@inheritDoc}
29+
*
30+
* @since x.x.x
31+
*/
32+
protected function input_schema(): array {
33+
return array(
34+
'type' => 'object',
35+
'properties' => array(
36+
'content' => array(
37+
'type' => 'string',
38+
'sanitize_callback' => 'sanitize_text_field',
39+
'description' => esc_html__( 'Content to generate excerpt suggestions for.', 'ai' ),
40+
),
41+
'post_id' => array(
42+
'type' => 'integer',
43+
'sanitize_callback' => 'absint',
44+
'description' => esc_html__( 'Content from this post will be used to generate excerpt suggestions. This overrides the content parameter if both are provided.', 'ai' ),
45+
),
46+
),
47+
);
48+
}
49+
50+
/**
51+
* {@inheritDoc}
52+
*
53+
* @since x.x.x
54+
*/
55+
protected function output_schema(): array {
56+
return array(
57+
'type' => 'string',
58+
'description' => esc_html__( 'Generated excerpt.', 'ai' ),
59+
);
60+
}
61+
62+
/**
63+
* {@inheritDoc}
64+
*
65+
* @since x.x.x
66+
*/
67+
protected function execute_callback( $input ) {
68+
// Default arguments.
69+
$args = wp_parse_args(
70+
$input,
71+
array(
72+
'content' => null,
73+
'post_id' => null,
74+
),
75+
);
76+
77+
// If a post ID is provided, ensure the post exists before using its' content.
78+
if ( $args['post_id'] ) {
79+
$post = get_post( $args['post_id'] );
80+
81+
if ( ! $post ) {
82+
return new WP_Error(
83+
'post_not_found',
84+
/* translators: %d: Post ID. */
85+
sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['post_id'] ) )
86+
);
87+
}
88+
89+
// Get the post context.
90+
$context = get_post_context( $args['post_id'] );
91+
92+
// Default to the passed in content if it exists.
93+
if ( $args['content'] ) {
94+
$context['content'] = normalize_content( $args['content'] );
95+
}
96+
} else {
97+
$context = array(
98+
'content' => normalize_content( $args['content'] ?? '' ),
99+
);
100+
}
101+
102+
// If we have no content, return an error.
103+
if ( empty( $context['content'] ) ) {
104+
return new WP_Error(
105+
'content_not_provided',
106+
esc_html__( 'Content is required to generate excerpt suggestions.', 'ai' )
107+
);
108+
}
109+
110+
// Generate the excerpts.
111+
$result = $this->generate_excerpt( $context );
112+
113+
// If we have an error, return it.
114+
if ( is_wp_error( $result ) ) {
115+
return $result;
116+
}
117+
118+
// If we have no results, return an error.
119+
if ( empty( $result ) ) {
120+
return new WP_Error(
121+
'no_results',
122+
esc_html__( 'No excerpt suggestions were generated.', 'ai' )
123+
);
124+
}
125+
126+
// Return the excerpts in the format the Ability expects.
127+
return sanitize_textarea_field( trim( $result, ' "\'' ) );
128+
}
129+
130+
/**
131+
* {@inheritDoc}
132+
*
133+
* @since x.x.x
134+
*/
135+
protected function permission_callback( $args ) {
136+
$post_id = isset( $args['post_id'] ) ? absint( $args['post_id'] ) : null;
137+
138+
if ( $post_id ) {
139+
$post = get_post( $args['post_id'] );
140+
141+
// Ensure the post exists.
142+
if ( ! $post ) {
143+
return new WP_Error(
144+
'post_not_found',
145+
/* translators: %d: Post ID. */
146+
sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['post_id'] ) )
147+
);
148+
}
149+
150+
// Ensure the user has permission to read this particular post.
151+
if ( ! current_user_can( 'read_post', $post_id ) ) {
152+
return new WP_Error(
153+
'insufficient_capabilities',
154+
esc_html__( 'You do not have permission to generate excerpts for this post.', 'ai' )
155+
);
156+
}
157+
158+
// Ensure the post type is allowed in REST endpoints.
159+
$post_type = get_post_type( $post_id );
160+
161+
if ( ! $post_type ) {
162+
return false;
163+
}
164+
165+
$post_type_obj = get_post_type_object( $post_type );
166+
167+
if ( ! $post_type_obj || empty( $post_type_obj->show_in_rest ) ) {
168+
return false;
169+
}
170+
} elseif ( ! current_user_can( 'edit_posts' ) ) {
171+
// Ensure the user has permission to edit posts in general.
172+
return new WP_Error(
173+
'insufficient_capabilities',
174+
esc_html__( 'You do not have permission to generate excerpts.', 'ai' )
175+
);
176+
}
177+
178+
return true;
179+
}
180+
181+
/**
182+
* {@inheritDoc}
183+
*
184+
* @since x.x.x
185+
*/
186+
protected function meta(): array {
187+
return array(
188+
'show_in_rest' => true,
189+
);
190+
}
191+
192+
/**
193+
* Generate an excerpt suggestion from the given content.
194+
*
195+
* @since x.x.x
196+
*
197+
* @param string|array<string, string> $context The context to generate an excerpt from.
198+
* @return string|\WP_Error The generated excerpt, or a WP_Error if there was an error.
199+
*/
200+
protected function generate_excerpt( $context ) {
201+
// Convert the context to a string if it's an array.
202+
if ( is_array( $context ) ) {
203+
$context = implode(
204+
"\n",
205+
array_map(
206+
static function ( $key, $value ) {
207+
return sprintf(
208+
'%s: %s',
209+
ucwords( str_replace( '_', ' ', $key ) ),
210+
$value
211+
);
212+
},
213+
array_keys( $context ),
214+
$context
215+
)
216+
);
217+
}
218+
219+
// Generate the excerpts using the AI client.
220+
return AI_Client::prompt_with_wp_error( '"""' . $context . '"""' )
221+
->using_system_instruction( $this->get_system_instruction() )
222+
->using_temperature( 0.7 )
223+
->using_model_preference( ...get_preferred_models() )
224+
->generate_text();
225+
}
226+
}

0 commit comments

Comments
 (0)