Skip to content

Conversation

@dkotter
Copy link
Collaborator

@dkotter dkotter commented Nov 3, 2025

What?

Partially closes #10

Builds off of the Title Generation feature base work done in #61 to wire up the actual execution of title generation. Note the actual UI still isn't in this PR, there will be a close followup that adds that.

Why?

In PR #61, we introduced the Title Generation Ability and Feature classes but the actual generation of titles wasn't completed there. This PR finishes that piece, though note there still isn't any UI to trigger this functionality, it all needs to be done via direct API requests.

How?

  • Pulls in the latest changes from the trunk branch of the WP AI Client repo to take advantage of the new settings screen and helper classes introduced there. This can be changed to pull from an actual release once that is out
  • Initializes the WP AI Client
  • Adds in a new API_Request class that can be used to make requests to an AI Provider. This ultimately utilizes the WP AI Client. Debated whether having this extra abstraction was worth it so can change this if desired. But it felt like the right approach knowing we'll have multiple other AI integrations coming that will all have slightly different requirements (i.e. arguments) they'll need to send
  • Adds a generate_titles method to the Title Generation Ability and uses that in the execute callback
  • Adds some helper functions to normalize content and get additional context we can send to the LLM
  • Adds functionality where we can store system instructions in their own PHP file that the Ability can then pull in. This could be hardcoded in the Ability class but felt cleaner to keep these in their own files for easier management

Testing Instructions

There is no UI yet but can be tested by making direct API requests to the Title Generation Ability run endpoint.

First you'll need to login to WordPress and go to Settings > AI Credentials and add in at least one API key.

Then make an authenticated POST request to the Title Generation run endpoint and ensure a title is generated based on the content you provide

curl --location 'https://example.com/wp-json/wp-abilities/v1/abilities/ai/title-generation/run' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic ****' \
--data '{
    "input": {
        "content": "this is some content!",
    }
}'

Make an authenticated POST request to the Title Generation run endpoint, passing in a valid post ID, and ensure a title is generated

curl --location 'https://example.com/wp-json/wp-abilities/v1/abilities/ai/title-generation/run' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic ****' \
--data '{
    "input": {
        "post_id": 1,
        "candidates": 1
    }
}'

Make an authenticated POST request to the Title Generation run endpoint, passing in a valid post ID and number and ensure multiple titles are generated

curl --location 'https://example.com/wp-json/wp-abilities/v1/abilities/ai/title-generation/run' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic ****' \
--data '{
    "input": {
        "post_id": 1,
        "candidates": 2
    }
}'

Make an authenticated POST request to the Title Generation run endpoint and don't pass any content or post ID and ensure an error is returned

curl --location 'https://example.com/wp-json/wp-abilities/v1/abilities/ai/title-generation/run' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic ****' \
--data '{
    "input": {
        "candidates": 2
    }
}'

Test using WordPress Playground

The changes in this pull request can be previewed and tested using this WordPress Playground instance:

Click here to test this pull request.

@dkotter dkotter added this to the 0.1.0 milestone Nov 3, 2025
@dkotter dkotter self-assigned this Nov 3, 2025
@codecov
Copy link

codecov bot commented Nov 3, 2025

Codecov Report

❌ Patch coverage is 63.24786% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.94%. Comparing base (2567e18) to head (7e9c935).
⚠️ Report is 78 commits behind head on trunk.

Files with missing lines Patch % Lines
...es/Abilities/Title_Generation/Title_Generation.php 21.73% 36 Missing ⚠️
includes/Abstracts/Abstract_Ability.php 71.42% 6 Missing ⚠️
includes/bootstrap.php 0.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##              trunk      #67       +/-   ##
=============================================
+ Coverage     48.48%   60.94%   +12.45%     
- Complexity       45       81       +36     
=============================================
  Files             6       10        +4     
  Lines           198      425      +227     
=============================================
+ Hits             96      259      +163     
- Misses          102      166       +64     
Flag Coverage Δ
unit 60.94% <63.24%> (+12.45%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jeffpaul
Copy link
Member

@dkotter with #61 merged in, looks like you're good to continue here with resolving the merge conflicts and getting this ready for review/testing

…nt now. Remove line changes in test files that weren't intended
@dkotter dkotter marked this pull request as ready for review November 13, 2025 20:42
@github-actions
Copy link

github-actions bot commented Nov 13, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @matysanchez, @prabinjha, @Meenakshi-bose.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Unlinked contributors: matysanchez, prabinjha, Meenakshi-bose.

Co-authored-by: dkotter <[email protected]>
Co-authored-by: JasonTheAdams <[email protected]>
Co-authored-by: felixarntz <[email protected]>
Co-authored-by: jeffpaul <[email protected]>
Co-authored-by: karmatosed <[email protected]>
Co-authored-by: justlevine <[email protected]>
Co-authored-by: swissspidy <[email protected]>
Co-authored-by: Ref34t <[email protected]>
Co-authored-by: t-hamano <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Copy link
Member

@JasonTheAdams JasonTheAdams left a comment

Choose a reason for hiding this comment

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

Excited to see the first usage of the WP AI Client, @dkotter!

I didn't fully review this, but I got confused enough by the architectural direction to pause here for some questions. I feel like this is over abstracted, and if it's not I'm really curious about the needs if not.

Comment on lines 228 to 237
/**
* Check if the AI SDK Client is available.
*
* @since 0.1.0
*
* @return bool True if the client is available, false otherwise.
*/
protected function is_client_available(): bool {
return class_exists( AI_Client::class );
}
Copy link
Member

Choose a reason for hiding this comment

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

Why wouldn't this be available?

Copy link
Member

Choose a reason for hiding this comment

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

I'm really confused by this class. Why do we need this? Why not just do AI_Client::prompt() in the Title_Generation Ability and do everything we need there? Maybe I'm missing something, but the whole abstraction of this class seems unnecessary to me.

I think the biggest thing it's doing is applying the model and provider, if specified. If that's all it's doing, then I'd make a function (or class) somewhere that just creates the Prompt_Builder, applies the Provider/Model preferences, and then returns the Prompt_Builder instance. That really leans into the point of the builder concept, where you can hand over a partially configured builder intsance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So I debated on this so happy to simplify. Few thoughts here:

Was trying to reduce the amount of code we would need to duplicate as we add functionality in the future that needs to make requests. Meaning for generating titles, beyond just passing in a prompt, we set the system instructions, we set the temperature value, we set the candidate count value and we set the default model preferences. I could easily move that out of this class and into the Title Generation Ability but we would then have that exact (or similar) code duplicated each time.

This class also standardizes how we process the response, ensuring we return an error if we get an empty array back and sanitizing the responses before returning. Again, all things that could happen in the Ability but would require us to duplicate that code each time.

That said, I'm fine with the approach of a helper function or two that will build the Prompt_Builder and return it and then each Ability/Feature/Experiment/Whatever that wants to make requests can take over from there. Let me make those changes and see if that feels like a better approach

Copy link
Member

Choose a reason for hiding this comment

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

I see where you're coming from. My main motivation here is that we want to think of these Abilities as examples, and I want devs to see how easy it is to construct a prompt and get the results in the ability.

If we need to normalize the response for our abilities in some way, then I'd like to do that after using the builder. That will also help us see if there's some sort of sanitization/normalization that we should introduce in the WP AI Client in some way.

Just to make sure you're aware, there's also a Prompt_Builder_With_WP_Error that can be used if you want to avoid exceptions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So I've simplified this a bit in ca74ff1, getting rid of the API_Request class and adding a helper function that builds our prompt builder instead.

If that still feels too abstracted (which I've gone back and forth on and can't decide), we can remove all of that and instead, just do this directly in the generate_titles method:

$prompt_builder = AI_Client::prompt_with_wp_error( '"""' . $context . '"""' );
$prompt_builder = $prompt_builder->using_system_instruction( $this->get_system_instruction() );
$prompt_builder = $prompt_builder->using_temperature( 0.7 );
$prompt_builder = $prompt_builder->using_candidate_count( (int) $candidates );
$prompt_builder = $prompt_builder->using_model_preference( ...get_preferred_models() );

That's obviously cleaner and a lot less code but does mean we would have very similar (if not exact) code for each experiment we add. But as you've said, would be great if people can look at the code here and easily understand how things work, even if that means we end up with some code duplication

Copy link
Member

Choose a reason for hiding this comment

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

Getting there! A couple notes:

  1. If you specify a model you don't need to specify a provider, as a model implies a provider
  2. You probably don't need to specify the preferred models array. The Prompt Builder will simply select a model that fits the prompt. Preferring a model is for when you have something like Nano Banana setup and you want to prefer that for image generation.

Also, chaining can be cleaned up a lot (without re-assigning the variable repeatedly):

$prompt_builder = AI_Client::prompt_with_wp_error( '"""' . $context . '"""' );
	->using_system_instruction( $this->get_system_instruction() );
	->using_temperature( 0.7 );
	->using_candidate_count( (int) $candidates );
	->using_model_preference( ...get_preferred_models() );

I don't think the helper is really needed, as I'm not really seeing any code duplication that's necessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay, more simplification done in 94dff32

You probably don't need to specify the preferred models array

The reason I added this is because I ran into this issue when testing: WordPress/php-ai-client#117. Basically things worked great one day and the next day things started to fail. In debugging, found out it was because the Prompt Builder started using the gpt-4o-mini-search-preview model which doesn't work properly for text generation.

So I can remove this but feels safer to have some defaults to avoid issues where things might break from one day to the next if new models are released or the models API results change.

Copy link
Member

Choose a reason for hiding this comment

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

Ohhh, that's good for us to know. I'm going to ping @felixarntz on this one and get his thoughts. That sounds like a potential bug for that model's capabilities in PHP AI Client.

Definitely let us know if you run into stuff like that. You're the first one bringing this all together, so there are quite likely bugs like this. 😆

Copy link
Member

Choose a reason for hiding this comment

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

I replied in WordPress/php-ai-client#117 (comment):

  • Concrete problem: Our OpenAI model capability and option assignments are probably inaccurate / out of date.
  • Underlying problem: We should not return the alphabetically first eligible model, but rather a flagship model of the provider that makes sense to prefer by default.

Copy link
Member

Choose a reason for hiding this comment

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

I'd say for now let's put a preference list here, and then remove it once we've fixed the upstream bug.

…ions. Use this new helper function to get a prompt builder and then use that to make our requests
Copy link
Member

@JasonTheAdams JasonTheAdams left a comment

Choose a reason for hiding this comment

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

Finished going through it! Left a few minor suggestions, but it's really close!

Comment on lines +77 to +83
/**
* TODO: Might be interesting to add simple Abilities for the following,
* just as a way to demonstrate a different approach to registering Abilities,
* how to call Abilities via PHP and how multiple Abilities can be used together.
*
* Example: Get post content Ability; get post author Ability; get post terms Ability.
*/
Copy link
Member

Choose a reason for hiding this comment

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

Agreed! I like the idea of composing abilities!

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'll open another PR after this merges that adds these in and we can continue conversation on that

Comment on lines 120 to 122
* Automatic detection order:
* 1. `system-instruction.php`
* 2. `prompt.php`
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious, why have more than one option at this time? Why not just stick with one to stick to a single convention?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch. Originally had this named as prompt and then renamed to match what the Prompt Builder expects and left support for both. But as this is a net-new thing, no need to support both right now. Removed in 8619a72

}
}

return '';
Copy link
Member

Choose a reason for hiding this comment

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

Do we want silent failure here? Maybe yes, just double-checking. 🙂

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That was the thought, yes, as the Prompt Builder handles that fine. Could change this to return an error that we output, though that would require us to not have the same chaining we have right now on the Prompt Builder as we'd need to check if an error was returned before calling using_system_instruction

Copy link
Member

@JasonTheAdams JasonTheAdams left a comment

Choose a reason for hiding this comment

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

Great work, @dkotter! LGTM! 🎉

Comment on lines +266 to +271
return AI_Client::prompt_with_wp_error( '"""' . $context . '"""' )
->using_system_instruction( $this->get_system_instruction() )
->using_temperature( 0.7 )
->using_candidate_count( (int) $candidates )
->using_model_preference( ...get_preferred_models() )
->generate_texts();
Copy link
Member

Choose a reason for hiding this comment

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

One note I'll make here that I don't think we need to do anything about at this time, is the use of the Prompt_Builder::is_supporteed_for_text_generation() method. This is intended to make it clear if there is not a model that supports this prompt's needs, allowing us to do something in that condition.

I want to add a WP_Ability::is_available() method in the future for checking ahead of time if an Ability will contextually work. I think this check would fit well there.

@dkotter dkotter merged commit a9cb31a into WordPress:trunk Nov 14, 2025
33 checks passed
@dkotter dkotter deleted the feature/title-generation-execute branch November 14, 2025 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Title Generation / Rewriting

4 participants