Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4a9c4d7
Add the base Title Generation class
dkotter Oct 30, 2025
475a827
Load Title Generation by default
dkotter Oct 30, 2025
dae9ef6
Update the Abilities API to the latest 0.4.0 release
dkotter Oct 30, 2025
96912e2
Register a custom Abilities category and register title generation as…
dkotter Oct 30, 2025
82319aa
Remove the custom REST endpoint as we can use the Abilities endpoint …
dkotter Oct 30, 2025
8b64bc9
Add some base unit tests
dkotter Oct 30, 2025
5de7754
Ignore PHPStan errors that aren't real
dkotter Oct 30, 2025
4f1f1c4
Minor cleanup
dkotter Oct 30, 2025
16c2a9c
Add an abstract Ability class that we can use to register abilities
dkotter Oct 30, 2025
aa0a956
Make some changes to the abstract ability class. Move all functionali…
dkotter Oct 30, 2025
70069f5
For now, register our single Abilities Category in the bootstrap
dkotter Oct 30, 2025
c34f992
Add a method to get the slug used when registering the ability
dkotter Oct 30, 2025
6594c5b
Try changing permissions to see if that fixes our PR comment workflow…
dkotter Oct 30, 2025
5607fce
Fix PHPCS and PHPStan errors
dkotter Oct 30, 2025
9aea208
One more workflow permission change test
dkotter Oct 30, 2025
79318d3
Revert workflow permission changes
dkotter Oct 30, 2025
dcec2f6
Add arguments to allow passing in a post ID, which if it matches a po…
dkotter Oct 30, 2025
a574ccb
Ensure we have content to process before we generate titles
dkotter Oct 31, 2025
88eb392
Add unit tests to cover new classes
dkotter Oct 31, 2025
e273802
Fix tests on trunk
dkotter Oct 31, 2025
1beac35
Actually fix tests on trunk
dkotter Oct 31, 2025
87bd000
Merge branch 'trunk' of github.com:WordPress/ai into feature/title-ge…
dkotter Nov 10, 2025
779d3d6
Remove the get_ability_slug method and instead directly set the Abili…
dkotter Nov 10, 2025
fff6c5d
Reference the Feature classes themselves when registering our default…
dkotter Nov 10, 2025
550b4fb
Remove the coupling of a Feature to an Ability and instead pass in th…
dkotter Nov 10, 2025
032a3e7
Add comment documenting why we are registering a generic Abilities ca…
dkotter Nov 10, 2025
381e2b7
Fix tests
dkotter Nov 10, 2025
86f1ebe
Declare strict inline types to new files, following changes made in #72
dkotter Nov 10, 2025
e0e4873
Add more robust permission checks. Better check on post ID before ret…
dkotter Nov 11, 2025
9b9b123
When checking permissions when a specific post ID is provided, check …
dkotter Nov 11, 2025
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"automattic/jetpack-autoloader": "^5.0",
"ext-json": "*",
"php": ">=7.4",
"wordpress/abilities-api": "^0.4.0-rc",
"wordpress/abilities-api": "^0.4.0",
"wordpress/mcp-adapter": "dev-trunk",
"wordpress/wp-ai-client": "dev-trunk"
},
Expand Down
12 changes: 6 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

172 changes: 172 additions & 0 deletions includes/Abilities/Title_Generation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php
/**
* Title generation WordPress Ability implementation.
*
* @package WordPress\AI
*/

declare( strict_types=1 );

namespace WordPress\AI\Abilities;

use WP_Error;
use WordPress\AI\Abstracts\Abstract_Ability;

/**
* Title generation WordPress Ability.
*
* @since 0.1.0
*/
class Title_Generation extends Abstract_Ability {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this topic came up in the initial repo scaffolding PR, but I don't understand why we should use abilities for something like this.

My understanding is that abilities are primarily meant for AI models to use WordPress, not that abilities use AI.

As such, I consider title generation a "feature". There will already be an ability to set the post title at some point, and that's the kind of thing I think should be exposed as an ability. That way, an agent could already generate a post title because the generative aspect of it is naturally covered.

Now, I may be mistaken here, and maybe anything you can do in WordPress should be an ability. But thinking from an AI tooling perspective, it feels a bit odd to me to have a tool more or less just to call a generative model when those tools are typically used by generative models anyway.

It would be great to get other leads to chime in here with their perspective on this. @Jameswlepage @swissspidy @JasonTheAdams

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can easily change this back to where each individual feature registers it's own REST API endpoint that will be used when the feature is triggered (that's initially what I started with).

That said, one of the reasons I ended up switching from that approach here is the Abilities API gives us all of that with basically the same code (registering, permission callbacks, execute callbacks, schema, etc). But in going with the ability approach, we accomplish a few additional things:

  1. We showcase how the Abilities API can be used, which I believe is one of the goals of this plugin
  2. We open up avenues for others to more easily build on top of this plugin. For example, if I wanted to build a plugin on top of this one that allows you to generate titles from the post list screen, I would just need to build my own UI then use the Abilities API to trigger the generate titles ability, either by calling the ability via PHP, calling the REST endpoint directly or eventually calling the client-side ability.
  3. If desired, we can expose this ability to an MCP server (that part isn't done in this PR). While I agree it feels odd to have an ability that an AI tool could call that would then itself call an LLM (why not just skip the middleman in most situations) I could see a scenario where someone is wanting to manage their WordPress content within something like Claude, where it can connect to your site, find content, suggest some titles and then update that content with the title you choose

Overall my goal here has been to keep things as extendable as possible, provide examples of how these AI tools can be used and hopefully inspire others to build their own things using these same tools (or build on top of what we do). Worth noting this is a slightly different approach then if I was trying to get something merged to WordPress core but leaning in to the "experiments" label here.

All that said, happy to change this approach if we think that's best. Really just want to get this unblocked so we can start to make some more serious progress here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all makes sense. I think the main caveat is regarding:

We showcase how the Abilities API can be used, which I believe is one of the goals of this plugin

Is the way the Abilities API is used here a good example of how it should be used or not? Going back to my original concern. That's where I'd like other people's feedback so that we can make a decision.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixarntz who else can we tag in for review here to get to a resolution and keep feature development unblocked in the plugin?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeffpaul Who I pinged above :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the WordPress source code[1]:

The Abilities API provides a unified, extensible framework for registering and executing discrete capabilities within WordPress. An "ability" is a self-contained unit of functionality with defined inputs, outputs, permissions, and execution logic.

To me, this makes it clear that abilities are about exposing functionality of WordPress (or plugins) but it leaves open the question of if that functionality can use an external source such as AI or any 3rd party API. Abilities aren't just about exposing functionality to AI, they can also be used (theoretically) in place such as wp-cli, command palate, or in a UI.

I think that generating titles is a discrete activity that can have a defined input, output, permissions, and execution and thus this is an acceptable use of the abilities API.

[1] I see a few specific word choices that I think need to get cleared up with this definition and will be opening a PR for that, but in general I think this is a good definition to use for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's certainly confusing when a tool or ability itself calls another AI tool. Especially when nothing of that is implemented yet and this is just boilerplate code.

This reminds me of a situation in the OG WP-CLI MCP implementation, where we implemented featured image generation as an MCP tool, and that tool made an AI call to generate an image. Ideally that wouldn't have been an MCP tool though, but done by the AI.

Likewise, the AI can already generate titles itself. It doesn't have to call a tool that calls an AI to generate titles. So in this context, a title generation tool call doesn't make sense. A Title Generation UI feature in the editor could generate a title using the in-browser AI model or call the WP PHP AI SDK REST API endpoint to do this work and then use the "Update Post" ability to update the title (or just use the editor APIs).

At the same time, however, I can also see how it's useful for a Title Generation ability to do all of that, so that any plugin could easily integrate the same functionality in their UI without reinventing the wheel.

Soo... both could be true? :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The committed definition that @aaronjorbin referenced is explicit and intentional; at the risk of being a broken record the Abilities API is about much more than AI, and definitely more than just MCP.

As far as the delineation between what is an Ability vs a (lowercase) feature vs a(n uppercase) Feature, etc. I think a good part of it is a naming things problem with the word "Features". If we rename to Experiment, we lessen the confusion, and reduce the need for future conversations like this one.

Soo... both could be true? :-)

IMO yes, just because we have an Ability doesn't mean that it needs to be exposed or used by MCP, and abilities can also be made up of other abilities. Long term, we could have a Title Generation Ability (with its own schema) that wraps a Content Generation Ability (that itself internally is just a interpolated prompt to an LLM), that are both toggleable via the same Content Generation Feature Experiment, with the former using WP_Ability::$metadata['annotations']['show_in_mcp'] = false, but show_in_cp = true (or whatever, the implementation details are less important than the concept of composability)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this is a very open-ended conversation that could go on and on.

I agree that Abilities API is "more than AI", yet here we are building it for AI, and I bet it wouldn't have come up as a new API if we weren't building out AI infra for WordPress. But yes, it can and probably should be used for other use-cases than AI agents.

On the other hand, I also agree with Pascal's point that it's weird to have a tool that purely calls an LLM to generate a title because that doesn't make much sense to provide as a tool to an LLM (since it can already do that on its own). For that context, we'd instead need tools to obtain the necessary context (post content etc.).

Maybe the answer is that it's fine for this to be an ability, but it would be an ability that is primarily relevant for other use-cases than AI agents - e.g. simply a way to implement a new WordPress feature ability using a standard API.

That brings up an important point though, related to the ongoing conversation how to expose the right tools to an agent: Arguably an ability (tool) like this should not be exposed to an agent (because of the above)? Does there need to be a way to mark such abilities? Or should it simply be handled via ability categories? cc @gziolo for awareness

Anyway, due to the nature of this discussion, I'm happy to unblock this and just stick with the ability implementation for now. The one thing we should revise though is to get rid of the get_ability_slug() method on the Feature interface and base class, to not prematurely couple the two.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi folks! 👋

I've somehow missed this PR. Thanks, @jeffpaul, for pinging me!

This is a perfectly valid use of Abilities, from my vantage point. I agree with @swissspidy on this one. It would be weird to expose this to MCP and then have an AI call a function that calls AI. In that case, you'd simply have an Ability for updating the post title (or whatever) which is exposed to MCP, and the model calls that while generating the title itself. But to have an ability that uses AI to generate a title is perfectly fine; I'd even be fine if this was two abilities:

  1. An ability for updating posts
  2. An ability for generating and returning a title string using AI

The first could potentially even reference the second with a "generate title" boolean input, or something like that. I'm honestly not thinking that hard about it, the point being abilities can be nice and composable. I could even see having a way of piping abilities in the future.

This actually softly brings up something I was thinking over yesterday: introducing an open-world annotation to abilities. I'm borrowing this concept from MCP, which is simply a way of saying "by the way, this function reaches outside of the application and does something externally". In this case, it would help clarify that this ability uses an external service — the AI model.

In short, I'm good with this!


/**
* Returns the category of the ability.
*
* @since 0.1.0
*
* @return string The category of the ability.
*/
protected function category(): string {
return 'ai-experiments'; // TODO: add a reusable way to get the category slug?
}

/**
* Returns the input schema of the ability.
*
* @since 0.1.0
*
* @return array<string, mixed> The input schema of the ability.
*/
protected function input_schema(): array {
return array(
'type' => 'object',
'properties' => array(
'content' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'description' => esc_html__( 'Content to generate title suggestions for.', 'ai' ),
),
'post_id' => array(
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Content from this post will be used to generate title suggestions. This overrides the content parameter if both are provided.', 'ai' ),
),
'n' => array(
'type' => 'integer',
'minimum' => 1,
'maximum' => 10,
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Number of titles to generate', 'ai' ),
),
),
);
}

/**
* Returns the output schema of the ability.
*
* @since 0.1.0
*
* @return array<string, mixed> The output schema of the ability.
*/
protected function output_schema(): array {
return array(
'type' => 'object',
'properties' => array(
'titles' => array(
'type' => 'array',
'description' => esc_html__( 'Generated title suggestions.', 'ai' ),
'items' => array(
'type' => 'string',
),
),
),
);
}

/**
* Executes the ability with the given input arguments.
*
* @since 0.1.0
*
* @param mixed $input The input arguments to the ability.
* @return mixed|\WP_Error The result of the ability execution, or a WP_Error on failure.
*/
protected function execute_callback( $input ) {
// Default arguments.
$args = wp_parse_args(
$input,
array(
'content' => null,
'post_id' => null,
'n' => 1,
),
);

// If a post ID is provided, ensure the post exists before using its' content.
if ( $args['post_id'] ) {
$post = get_post( $args['post_id'] );

if ( ! $post ) {
return new WP_Error(
'post_not_found',
/* translators: %d: Post ID. */
sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['post_id'] ) )
);
}

$args['content'] = $post->post_content;
}

// If we have no content, return an error.
if ( ! $args['content'] ) {
return new WP_Error(
'content_not_provided',
esc_html__( 'Content is required to generate title suggestions.', 'ai' )
);
}

// TODO: Implement the title generation logic.

return array(
'name' => $this->get_name(),
'label' => $this->get_label(),
'description' => $this->get_description(),
'content' => wp_kses_post( $args['content'] ),
'post_id' => absint( $args['post_id'] ) ?? esc_html__( 'Not provided', 'ai' ),
'n' => absint( $args['n'] ),
);
}

/**
* Returns the permission callback of the ability.
*
* @since 0.1.0
*
* @param mixed $args The input arguments to the ability.
* @return bool|\WP_Error True if the user has permission, WP_Error otherwise.
*/
protected function permission_callback( $args ) {
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'insufficient_capabilities',
esc_html__( 'You do not have permission to generate titles.', 'ai' )
);
}

return true;
}

/**
* Returns the meta of the ability.
*
* @since 0.1.0
*
* @return array<string, mixed> The meta of the ability.
*/
protected function meta(): array {
return array(
'show_in_rest' => true,
);
}
}
100 changes: 100 additions & 0 deletions includes/Abstracts/Abstract_Ability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
/**
* Abstract Ability base class.
*
* @package WordPress\AI\Abstracts
*/

declare( strict_types=1 );

namespace WordPress\AI\Abstracts;

use WP_Ability;

/**
* Base implementation for a WordPress Ability.
*
* @since 0.1.0
*/
abstract class Abstract_Ability extends WP_Ability {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm strongly against extending WP_Ability like this.

If there's justification for an abstract class, then we should follow the pattern of Abstract_REST_Controller, and use this class to build the args that get passed to a wp_register_ability() call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have discussed this many times before: It's fine to extend WP_Ability, it's allowed by the API. This is a personal preference.

Copy link
Contributor

@justlevine justlevine Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except we're in the WordPress/ai repo so it's not only personal preference, and just because something is "allowed" doesn't means it should be done without defined intent, let alone encouraged, or in this case enforced.

(Which I believe is a more accurate summary of the discussion, most recently iirc WordPress/abilities-api#97 ).

So, for our specific plugin / this PR:

  1. Why do we need an abstract wrapper for registering abilities at all, especially while there is (currently) only one ability in this codebase?

  2. Why is extending WP_Ability the ideal path to accomplish No. 1?

The two biggest downsides of extending WP_Ability in our particular case (as noted, the general issues with the pattern have been repeatedly discussed):

  1. Instead of dogfooding the entire Abilities API, we're diverging from it, which beyond the testing/future-compat issues, also make it significantly harder to "graduate" an experimental ability to Core.
  2. Practically with this approach, every feature that registers an ability needs to duplicate the same registration code.

(IMO the Abstract_REST_Controller pattern addresses both of those downsides, although any extra layers of prescriptive abstraction inside a Feature this early should still cause some pause, considering the whole purpose of this repo is to "experiment" )

Copy link
Member

@felixarntz felixarntz Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an API allows something, it's fine to do it. It doesn't mean it's encouraged, and it doesn't mean it's discouraged. If the API wants it to be discouraged, it shouldn't even be possible in the first place.

This project, like any other project, has to choose one of those two approaches, and which approach is chosen is personal preference. Your personal preference is to not extend WP_Ability, my personal preference is to extend it.

To your questions:

  1. This PR implements some scaffolding. Based on the other discussion, it looks like we're leaning more or less any feature in WordPress can be an ability, so it makes sense to provide scaffolding for how to implement abilities in the scope of this plugin.
  2. This project is mostly object-oriented, so using classes in general makes sense. If you don't extend WP_Ability, we'd probably still want one class per ability to keep things decoupled, especially because the features (or experiments) that make use of the abilities are decoupled. WP_Ability provides a possible foundation for this class breakdown, so IMO it makes sense to use it.

Many aspects of coding are subjective. Whether this project uses procedural programming or OOP is subjective, and so is whether it extends WP_Ability or does something else. We can reason about it, but there's no "ideal" and no "right or wrong".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weighing in here, I kindly suggest we step back from the "is extending WP_Ability good or bad" and such debate. To @felixarntz's point, it's part of the API and so everything here is a perfectly sensible use of that, and even a great way of showing how it can be done in a good, object-oriented manner. I like it.

That said, I do think it would be a good idea to also include a procedural example in this project, which we can reference in the documentation as something intentional. That way we can help both procedurally and OOP inclined folks on how they can interact with the Abilities API in their preferred manner.

In short, I think this is a great both-and situation where we can show off how flexibly extensible this API is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lively conversation! Appreciate the passion and continued clarification on both sides! 😄

Thanks for providing the example and further context, @justlevine! It helps! I took some time to reflect and really tried to think through what you're suggesting.

I think I've somewhat returned back to my original point, but with some clarification. That is, providing two examples as part of this repo for educational purposes:

  1. Extending WP_Ability as a way of showing how to use ability_class
  2. Using wp_register_ability() in a purely, non-cleverly procedural way

This makes me like the way this works even more. The idea of having an Abstract_Ability that doesn't actually extend WP_Ability feels, to me, even more abstract than this, and doesn't illustrate using ability_class. It's just a procedural way of registering that's wrapped in OOP. I'm also not a fan of bringing the Feature into the Ability level, as I think the Ability is a lower-level concept which should be a dependency of the Feature, not the other way around.

In conclusion, my preference is how this currently works because it illustrates ability_class. I would suggest that one of the next features register the Ability in a procedural way, not tucking it into OOP, as a simple example for folks to follow.

Copy link
Contributor

@justlevine justlevine Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkotter

it seems at a minimum:

  • Keep the Abstract_Ability class but don't extend WP_Ability
  • Keep most of the other methods the same in both the Abstract_Ability class and any class that extends that (just Title_Generation in this case)
  • Look at adding a registration method in Abstract_Ability to make it easier for Abilities to be registered

Does that seem accurate or are there other parts I'm missing that should be modified?

Yup, that's all of it 🙇. Basically the WP_Rest_Controller pattern, but with ergonomics that make sense for us.


It's just a procedural way of registering that's wrapped in OOP

@JasonTheAdams I'm not entirely sure I understood what you mean here. Can you clarify?

At risk of being redundant, we wouldn't be testing or demonstrating ability_class inside Abstract_Ability; instead we're demonstrating wp_register_ability() and how to use it in an OOP manner, and testing the entirety of \WP_Ability (futureproofing). A specific experiment that wants to extends \WP_Ability to use ability_class is less code/maintenance. And the need to extend \WP_Ability (e.g. to overload behavior, not to have OOP ergonomics when creating new abilities) is much less likely than wanting WP_Ability that can opt-into to the entire lifecycle and not worry about future-compat or cross-compat with other experiments.

IOW this doesn't preclude ability_class, it's (imo) easier and more robust to illustrate and experiment with extends \WP_Ability when it's intentional versus when its enforced plugin-wide as an Abstract_* pattern.

(If that was already clear apologies, I'm signing out for the night, and didn't want to hold up the convo with another round trip 😅)

Copy link
Contributor

@justlevine justlevine Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally would have implemented it somewhat similarly.

( @felixarntz In fairness one of the main reasons I introduced ability_class was to keep similar with your prior art and minimal effort in adapting for the WordCamp demo. So really you could say it's because you would have (and did) implemented it similarly that the pattern is supported to begin with 😅)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate all the input, @justlevine, and referencing this as following the WP_Rest_Controller pattern definitely helped in understanding where you're coming from. I confess I'm not quite convinced that should replace this. I can see it as another pattern, and certainly one with history, but I remain in the camp that I like this as an example of an Ability being extended and registered outside of itself as an ability_class.

At this point, I think we need to merge this PR and can revisit this in a subsequent PR. As I've said, I'm open to having multiple ways of registering abilities to show folks the versatility of registration. This isn't a dismissal, @justlevine, as we've spent a good amount of time on this and the majority of folks contributing to the conversation remain unconvinced, so we ought to move forward. Please consider making a PR to introduce another Feature which we can contrast this with.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @JasonTheAdams , and I agree that iterating quickly is the goal here and happy to kick the discussion to a follow-up PR so we can keep progressing.

Correct me if I'm wrong (cc @jeffpaul ), but my gut says that we have at least until 6.9 ships - if not a bit longer - before any architectural decisions or reversing them have any real impact. (Tangentially, we're also overdue a discussion about WPCS, contribution guidelines/barriers to entry for new modules/experiments, and what a path to core for specific experiments/features would look like)


/**
* Constructor.
*
* @since 0.1.0
*
* @param string $name The name of the ability.
* @param array<string,mixed> $properties The properties of the ability. Must include `label`.
*/
public function __construct( string $name, array $properties = array() ) {
parent::__construct(
$name,
array(
'label' => $properties['label'] ?? '',
'description' => $properties['description'] ?? '',
'category' => $this->category(),
'input_schema' => $this->input_schema(),
'output_schema' => $this->output_schema(),
'execute_callback' => array( $this, 'execute_callback' ),
'permission_callback' => array( $this, 'permission_callback' ),
'meta' => $this->meta(),
)
);
}

/**
* Returns the category of the ability.
*
* @since 0.1.0
*
* @return string The category of the ability.
*/
abstract protected function category(): string;

/**
* Returns the input schema of the ability.
*
* @since 0.1.0
*
* @return array<string, mixed> The input schema of the ability.
*/
abstract protected function input_schema(): array;

/**
* Returns the output schema of the ability.
*
* @since 0.1.0
*
* @return array<string, mixed> The output schema of the ability.
*/
abstract protected function output_schema(): array;

/**
* Executes the ability with the given input arguments.
*
* @since 0.1.0
*
* @param mixed $input The input arguments to the ability.
* @return mixed|\WP_Error The result of the ability execution, or a WP_Error on failure.
*/
abstract protected function execute_callback( $input );

/**
* Checks whether the current user has permission to execute the ability with the given input arguments.
*
* @since 0.1.0
*
* @param mixed $input The input arguments to the ability.
* @return bool|\WP_Error True if the user has permission, WP_Error otherwise.
*/
abstract protected function permission_callback( $input );

/**
* Returns the meta of the ability.
*
* @since 0.1.0
*
* @return array<string, mixed> The meta of the ability.
*/
abstract protected function meta(): array;
}
3 changes: 2 additions & 1 deletion includes/Feature_Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ public function register_default_features(): void {
*/
private function get_default_features(): array {
$feature_classes = array(
'WordPress\AI\Features\Example_Feature\Example_Feature',
\WordPress\AI\Features\Example_Feature\Example_Feature::class,
\WordPress\AI\Features\Title_Generation\Title_Generation::class,
);

/**
Expand Down
61 changes: 61 additions & 0 deletions includes/Features/Title_Generation/Title_Generation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* Title generation feature implementation.
*
* @package WordPress\AI
*/

declare( strict_types=1 );

namespace WordPress\AI\Features\Title_Generation;

use WordPress\AI\Abilities\Title_Generation as Title_Generation_Ability;
use WordPress\AI\Abstracts\Abstract_Feature;

/**
* Title generation feature.
*
* @since 0.1.0
*/
class Title_Generation extends Abstract_Feature {

/**
* {@inheritDoc}
*
* @since 0.1.0
*
* @return array{id: string, label: string, description: string} Feature metadata.
*/
protected function load_feature_metadata(): array {
return array(
'id' => 'title-generation',
'label' => __( 'Title Generation', 'ai' ),
'description' => __( 'Generates title suggestions from content', 'ai' ),
);
}

/**
* {@inheritDoc}
*
* @since 0.1.0
*/
public function register(): void {
add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
}

/**
* Registers any needed abilities.
*
* @since 0.1.0
*/
public function register_abilities(): void {
wp_register_ability(
'ai/' . $this->get_id(),
array(
'label' => $this->get_label(),
'description' => $this->get_description(),
'ability_class' => Title_Generation_Ability::class,
),
);
}
}
Loading