Skip to content

Commit 2b9dde0

Browse files
jeffpauldkotterJasonTheAdams
authored
Merge pull request WordPress#86 from dkotter/feature/utility-abilities
Register some utility Abilities Co-authored-by: dkotter <dkotter@git.wordpress.org> Co-authored-by: JasonTheAdams <jason_the_adams@git.wordpress.org>
2 parents 076cb34 + 38055ba commit 2b9dde0

5 files changed

Lines changed: 448 additions & 47 deletions

File tree

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
<?php
2+
/**
3+
* Post-related WordPress Abilities.
4+
*
5+
* @package WordPress\AI
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace WordPress\AI\Abilities\Utilities;
11+
12+
use WP_Error;
13+
14+
/**
15+
* Post utility WordPress Abilities.
16+
*
17+
* @since 0.1.0
18+
*/
19+
class Posts {
20+
21+
/**
22+
* The fields that we support.
23+
*
24+
* @since 0.1.0
25+
* @var array<string>
26+
*/
27+
private static array $post_details_fields = array( 'content', 'title', 'slug', 'author', 'type', 'excerpt' );
28+
29+
/**
30+
* Register any needed hooks.
31+
*
32+
* @since 0.1.0
33+
*/
34+
public function register(): void {
35+
add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
36+
}
37+
38+
/**
39+
* Registers any needed abilities.
40+
*
41+
* @since 0.1.0
42+
*/
43+
public function register_abilities(): void {
44+
$this->register_get_post_details_ability();
45+
$this->register_get_terms_ability();
46+
}
47+
48+
/**
49+
* Registers the get-post-details ability.
50+
*
51+
* @since 0.1.0
52+
*/
53+
private function register_get_post_details_ability(): void {
54+
wp_register_ability(
55+
'ai/get-post-details',
56+
array(
57+
'label' => esc_html__( 'Get post details', 'ai' ),
58+
'description' => esc_html__( 'Get the details of a post based on the post ID. Optionally, limit the details to specific fields.', 'ai' ),
59+
'category' => AI_DEFAULT_ABILITY_CATEGORY,
60+
'input_schema' => array(
61+
'type' => 'object',
62+
'properties' => array(
63+
'post_id' => array(
64+
'type' => 'integer',
65+
'description' => esc_html__( 'The ID of the post to get the details of.', 'ai' ),
66+
),
67+
'fields' => array(
68+
'type' => 'array',
69+
'description' => esc_html__( 'The fields to get the details of. Will default to all fields if not provided.', 'ai' ),
70+
'items' => array(
71+
'type' => 'string',
72+
'enum' => self::$post_details_fields,
73+
),
74+
),
75+
),
76+
'required' => array( 'post_id' ),
77+
),
78+
'output_schema' => array(
79+
'type' => 'object',
80+
'description' => esc_html__( 'The details of the post.', 'ai' ),
81+
'properties' => array(
82+
'content' => array(
83+
'type' => 'string',
84+
'description' => esc_html__( 'The content of the post.', 'ai' ),
85+
),
86+
'title' => array(
87+
'type' => 'string',
88+
'description' => esc_html__( 'The title of the post.', 'ai' ),
89+
),
90+
'slug' => array(
91+
'type' => 'string',
92+
'description' => esc_html__( 'The slug of the post.', 'ai' ),
93+
),
94+
'author' => array(
95+
'type' => 'string',
96+
'description' => esc_html__( 'The author of the post.', 'ai' ),
97+
),
98+
'type' => array(
99+
'type' => 'string',
100+
'description' => esc_html__( 'The type of the post.', 'ai' ),
101+
),
102+
'excerpt' => array(
103+
'type' => 'string',
104+
'description' => esc_html__( 'The excerpt of the post.', 'ai' ),
105+
),
106+
),
107+
),
108+
'execute_callback' => static function ( array $input ) {
109+
$post_id = absint( $input['post_id'] );
110+
$post = self::get_post_object( $post_id );
111+
112+
// If the post doesn't exist, return an error.
113+
if ( is_wp_error( $post ) ) {
114+
return $post;
115+
}
116+
117+
// See if we have specific fields to get or default to all fields.
118+
$fields = isset( $input['fields'] ) && ! empty( $input['fields'] ) ? (array) $input['fields'] : self::$post_details_fields;
119+
120+
$details = array();
121+
122+
if ( in_array( 'content', $fields, true ) ) {
123+
$details['content'] = $post->post_content;
124+
}
125+
126+
if ( in_array( 'title', $fields, true ) ) {
127+
$details['title'] = $post->post_title;
128+
}
129+
130+
if ( in_array( 'slug', $fields, true ) ) {
131+
$details['slug'] = $post->post_name;
132+
}
133+
134+
if ( in_array( 'author', $fields, true ) ) {
135+
// Get the author display name.
136+
$author = get_user_by( 'ID', $post->post_author );
137+
if ( $author ) {
138+
$details['author'] = $author->display_name;
139+
} else {
140+
$details['author'] = '';
141+
}
142+
}
143+
144+
if ( in_array( 'type', $fields, true ) ) {
145+
$details['type'] = $post->post_type;
146+
}
147+
148+
if ( in_array( 'excerpt', $fields, true ) ) {
149+
$details['excerpt'] = $post->post_excerpt;
150+
}
151+
152+
// Return the post details.
153+
return $details;
154+
},
155+
'permission_callback' => array( $this, 'permission_callback' ),
156+
'meta' => array(
157+
'mcp' => array(
158+
'public' => true,
159+
'type' => 'tool',
160+
),
161+
),
162+
)
163+
);
164+
}
165+
166+
/**
167+
* Registers the get-terms ability.
168+
*
169+
* @since 0.1.0
170+
*/
171+
private function register_get_terms_ability(): void {
172+
wp_register_ability(
173+
'ai/get-post-terms',
174+
array(
175+
'label' => esc_html__( 'Get the post terms', 'ai' ),
176+
'description' => esc_html__( 'Get the terms of a post based on the post ID and optionally filter by taxonomy.', 'ai' ),
177+
'category' => AI_DEFAULT_ABILITY_CATEGORY,
178+
'input_schema' => array(
179+
'type' => 'object',
180+
'properties' => array(
181+
'post_id' => array(
182+
'type' => 'integer',
183+
'description' => esc_html__( 'The ID of the post to get the terms of.', 'ai' ),
184+
),
185+
'taxonomy' => array(
186+
'type' => 'string',
187+
'description' => esc_html__( 'The taxonomy to filter the terms by.', 'ai' ),
188+
),
189+
),
190+
'required' => array( 'post_id' ),
191+
),
192+
'output_schema' => array(
193+
'type' => 'object',
194+
'description' => esc_html__( 'An array of WP_Term objects assigned to the post.', 'ai' ),
195+
'properties' => array(
196+
'type' => 'array',
197+
'items' => array(
198+
'type' => 'array',
199+
'items' => array(
200+
'term_id' => array(
201+
'type' => 'integer',
202+
'description' => esc_html__( 'The ID of the term.', 'ai' ),
203+
),
204+
'name' => array(
205+
'type' => 'string',
206+
'description' => esc_html__( 'The name of the term.', 'ai' ),
207+
),
208+
'slug' => array(
209+
'type' => 'string',
210+
'description' => esc_html__( 'The slug of the term.', 'ai' ),
211+
),
212+
'term_group' => array(
213+
'type' => 'integer',
214+
'description' => esc_html__( 'The group ID of the term.', 'ai' ),
215+
),
216+
'term_taxonomy_id' => array(
217+
'type' => 'integer',
218+
'description' => esc_html__( 'The taxonomy ID of the term.', 'ai' ),
219+
),
220+
'taxonomy' => array(
221+
'type' => 'string',
222+
'description' => esc_html__( 'The taxonomy name of the term.', 'ai' ),
223+
),
224+
'description' => array(
225+
'type' => 'string',
226+
'description' => esc_html__( 'The description of the term.', 'ai' ),
227+
),
228+
'parent' => array(
229+
'type' => 'integer',
230+
'description' => esc_html__( 'The parent ID of the term.', 'ai' ),
231+
),
232+
'count' => array(
233+
'type' => 'integer',
234+
'description' => esc_html__( 'How many times the term is used.', 'ai' ),
235+
),
236+
'filter' => array(
237+
'type' => 'string',
238+
'description' => esc_html__( 'How the term should be filtered.', 'ai' ),
239+
),
240+
),
241+
),
242+
),
243+
),
244+
'execute_callback' => static function ( array $input ) {
245+
$post_id = absint( $input['post_id'] );
246+
$post = self::get_post_object( $post_id );
247+
248+
if ( is_wp_error( $post ) ) {
249+
return $post;
250+
}
251+
252+
// See if we have a specific taxonomy to get terms for.
253+
$taxonomy = $input['taxonomy'] ?? '';
254+
255+
if ( $taxonomy ) {
256+
// If a taxonomy is provided, ensure it exists.
257+
$taxonomy = get_taxonomy( $taxonomy );
258+
if ( ! $taxonomy ) {
259+
return new WP_Error(
260+
'taxonomy_not_found',
261+
esc_html__( 'Taxonomy not found.', 'ai' )
262+
);
263+
}
264+
$taxonomies = array( $taxonomy );
265+
} else {
266+
$taxonomies = get_object_taxonomies( $post->post_type, 'objects' );
267+
}
268+
269+
// Remove any taxonomies that are not allowed.
270+
$allowed_taxonomies = array();
271+
foreach ( $taxonomies as $taxonomy ) {
272+
// If the taxonomy is not allowed in REST endpoints, skip it.
273+
if ( empty( $taxonomy->show_in_rest ) ) {
274+
continue;
275+
}
276+
277+
// If the requested post isn't associated with this taxonomy, skip it.
278+
if ( ! is_object_in_taxonomy( $post->post_type, $taxonomy->name ) ) {
279+
continue;
280+
}
281+
282+
$allowed_taxonomies[] = $taxonomy->name;
283+
}
284+
285+
$terms = wp_get_object_terms( $post_id, $allowed_taxonomies );
286+
287+
if ( is_wp_error( $terms ) ) {
288+
return new WP_Error(
289+
'get_terms_error',
290+
/* translators: %1$s: Error message. */
291+
sprintf( esc_html__( 'Error getting terms: %1$s', 'ai' ), $terms->get_error_message() )
292+
);
293+
}
294+
295+
return $terms;
296+
},
297+
'permission_callback' => array( $this, 'permission_callback' ),
298+
'meta' => array(
299+
'mcp' => array(
300+
'public' => true,
301+
'type' => 'tool',
302+
),
303+
),
304+
),
305+
);
306+
}
307+
308+
/**
309+
* The default permission callback abilities can use.
310+
*
311+
* @since 0.1.0
312+
*
313+
* @param array<string, mixed> $args The input arguments to the ability.
314+
* @return bool|\WP_Error True or false depending on whether the user has permission; WP_Error if the post doesn't exist.
315+
*/
316+
public function permission_callback( array $args ) {
317+
$post_id = absint( $args['post_id'] );
318+
$post = self::get_post_object( $post_id );
319+
320+
// Ensure the post exists.
321+
if ( is_wp_error( $post ) ) {
322+
return $post;
323+
}
324+
325+
// Return true if the user has permission to read the post.
326+
return current_user_can( 'read_post', $post_id );
327+
}
328+
329+
/**
330+
* Gets the post object.
331+
*
332+
* @since 0.1.0
333+
*
334+
* @param int $post_id The ID of the post to get the object of.
335+
* @return \WP_Post|\WP_Error The post object or WP_Error if the post doesn't exist.
336+
*/
337+
private static function get_post_object( int $post_id ) {
338+
$post = get_post( $post_id );
339+
340+
// If the post doesn't exist, return an error.
341+
if ( ! $post ) {
342+
return new WP_Error(
343+
'post_not_found',
344+
esc_html__( 'Post not found.', 'ai' )
345+
);
346+
}
347+
348+
return $post;
349+
}
350+
}

includes/Abstracts/Abstract_Ability.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function __construct( string $name, array $properties = array() ) {
5151
* @return string The category of the ability.
5252
*/
5353
protected function category(): string {
54-
return 'ai-experiments';
54+
return AI_DEFAULT_ABILITY_CATEGORY;
5555
}
5656

5757
/**

includes/bootstrap.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace WordPress\AI;
1313

14+
use WordPress\AI\Abilities\Utilities\Posts;
1415
use WordPress\AI\Settings\Settings_Page;
1516
use WordPress\AI\Settings\Settings_Registration;
1617
use WordPress\AI_Client\AI_Client;
@@ -39,6 +40,9 @@
3940
if ( ! defined( 'AI_MIN_WP_VERSION' ) ) {
4041
define( 'AI_MIN_WP_VERSION', '6.8' );
4142
}
43+
if ( ! defined( 'AI_DEFAULT_ABILITY_CATEGORY' ) ) {
44+
define( 'AI_DEFAULT_ABILITY_CATEGORY', 'ai-experiments' );
45+
}
4246

4347
/**
4448
* Displays an admin notice for version requirement failures.
@@ -192,6 +196,10 @@ function initialize_experiments(): void {
192196
// Initialize the WP AI Client.
193197
AI_Client::init();
194198

199+
// Register our post-related WordPress Abilities.
200+
$post_abilities = new Posts();
201+
$post_abilities->register();
202+
195203
add_action(
196204
'wp_abilities_api_categories_init',
197205
static function () {
@@ -201,7 +209,7 @@ static function () {
201209
* in the future if we need/want more specific categories.
202210
*/
203211
wp_register_ability_category(
204-
'ai-experiments',
212+
AI_DEFAULT_ABILITY_CATEGORY,
205213
array(
206214
'label' => __( 'AI Experiments', 'ai' ),
207215
'description' => __( 'Various AI experiments.', 'ai' ),

0 commit comments

Comments
 (0)