Skip to content

Prompt template extension points - #770

Merged
dkotter merged 11 commits into
WordPress:developfrom
the-hercules:feature/prompt-template-extension-points
Jul 20, 2026
Merged

Prompt template extension points#770
dkotter merged 11 commits into
WordPress:developfrom
the-hercules:feature/prompt-template-extension-points

Conversation

@the-hercules

@the-hercules the-hercules commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Add prompt template extension points

Summary

Introduces consistent, scoped extension points so developers can customize the prompts
and model configuration of AI abilities without forking the plugin. Adds three
per-ability filter hooks plus a developer guide.

Closes #192

What changed

A single slug helper on Abstract_Ability derives a hook-safe slug from each ability
name (ai/title-generationtitle_generation), and three filters are wired through
every AI ability:

  • wpai_{slug}_system_instruction — override/extend the system instruction for one
    ability. Added once in Abstract_Ability::get_system_instruction(), running after
    the existing global wpai_system_instruction filter.
  • wpai_{slug}_prompt — modify the assembled user prompt before it is sent. Added to
    the 10 abilities that did not already expose one.
  • wpai_{slug}_prompt_builder — adjust temperature, model/provider preference, request
    options, etc. Added to all 12 prompt-building abilities, after the default model
    preference is applied and before generation support is verified.

Abilities covered: Title, Excerpt, Summarization, Content Resizing, Meta Description,
Editorial Updates, Editorial Notes, Alt Text, Image Prompt Generation, Image Generation,
Comment Analysis, Content Classification.

Where abilities carry extra context, it is passed as additional filter arguments
(e.g. $length for summarization, $block_type for editorial notes, $action for content
resizing).

Backward compatibility

  • The global wpai_system_instruction filter is unchanged; the new scoped filter runs after it.
  • meta_description and content_classification already shipped a wpai_{slug}_prompt
    filter with richer signatures ($content/$title, taxonomy args). Those are left exactly
    as-is
    — no duplicate hook is registered — so existing integrations keep working. They are
    documented alongside the new hooks.
  • wpai_meta_description_result_temperature still applies; the new meta-description builder
    filter runs after it.
  • Every new hook is additive and a no-op unless a callback is attached, so default behavior is
    byte-for-byte unchanged.

Design notes / decisions

A few decisions were made in the absence of issue feedback (happy to adjust):

  1. Scoped system-instruction hook included. The issue asks for per-ability system
    instruction control; the global filter alone would require name-branching.
  2. Image Generation included for symmetry. Its _prompt is the image prompt (not post
    content), and editorial guidelines are appended after the filter — documented as such.
  3. Builder filter fires after set_provider_model_preference() so it can intentionally
    override the developer-mode model choice without breaking the default path.
  4. Structured-output abilities (comment_analysis, content_classification,
    editorial_notes) document that the builder should be extended, not replaced, to preserve
    JSON response schemas.
Open WordPress Playground Preview

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

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.

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

Co-authored-by: the-hercules <thehercules@git.wordpress.org>
Co-authored-by: dkotter <dkotter@git.wordpress.org>
Co-authored-by: jeffpaul <jeffpaul@git.wordpress.org>

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

@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.68%. Comparing base (b15cb96) to head (ca265c1).

Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #770      +/-   ##
=============================================
+ Coverage      78.44%   79.68%   +1.24%     
- Complexity      2454     2460       +6     
=============================================
  Files            104      104              
  Lines           9925     9955      +30     
=============================================
+ Hits            7786     7933     +147     
+ Misses          2139     2022     -117     
Flag Coverage Δ
unit 79.68% <100.00%> (+1.24%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 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.

@dkotter dkotter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

While the changes in this PR work, I think it may be worth discussing or exploring an alternative approach. Right now each individual Ability is responsible for ensuring content is passed through a filter. So as new Abilities are added, we'll want to ensure they get these same filters.

I'm wondering if it would be better / easier to maintain if these filters were automatically added. As an example, if we introduce a get_prompt_builder method on the Abstract_Ability class, the filter could live there and get automatically applied when each individual Ability uses that method. That would reduce the amount of code changes in this PR and would ensure any new Abilities in the future automatically get those filters

@jeffpaul jeffpaul added this to the Future Release milestone Jun 26, 2026
@the-hercules

Copy link
Copy Markdown
Contributor Author

Hi @dkotter
Thanks for the review! You make a great point about maintainability and relying on developers to remember to manually add apply_filters into every new ability is definitely a recipe for them getting missed over time.

I looked into moving get_prompt_builder() entirely into Abstract_Ability so it's handled 100% automatically as you suggested. The tricky part is that different abilities configure the Prompt_Builder in very specific ways before the filter is meant to run. For example:

  • Summarization sets ->using_temperature( 0.9 )
  • Content_Classification sets ->as_json_response( ... )
  • Generate_Image sets RequestOptions with a 90s timeout and attaches reference images.

If Abstract_Ability::get_prompt_builder() creates the builder and immediately fires the filter hook, individual abilities would have to append their configurations afterwards. That would defeat the purpose of the hooks, because third-party developers wouldn't be able to override things like temperature if the ability hardcodes it after the filter runs.

Proposed Compromise
Instead of a single get_prompt_builder() that builds from scratch, what if we centralize the filter logic into a helper method in Abstract_Ability?

We could introduce a filter_prompt_builder() method on the base class:

protected function filter_prompt_builder( \WP_AI_Client_Prompt_Builder $builder, string $feature_class, array $fallback_models = array(), ...$filter_args ) {
    // 1. Apply model preferences
    $builder = $this->set_provider_model_preference( $builder, $feature_class, $fallback_models );
    
    // 2. Automatically apply the builder filter
    return apply_filters( "wpai_{$this->get_ability_slug()}_prompt_builder", $builder, ...$filter_args );
}

Then, an individual ability simply configures its specific settings and passes the builder through the helper:

$prompt_builder = wp_ai_client_prompt( $prompt )
    ->using_system_instruction( $this->get_system_instruction() )
    ->using_temperature( 0.7 );

// One standardized call that applies the model fallbacks AND fires the filter hook
$prompt_builder = $this->filter_prompt_builder( $prompt_builder, Title_Generation_Experiment::class, array(), $prompt );

We could do something similar for the prompt string itself with a filter_prompt() helper.

This drastically reduces the boilerplate across the abilities and centralizes the hook logic. While new abilities will still have to call $this->filter_prompt_builder(), they already have to call $this->set_provider_model_preference() anyway, so combining them makes it very hard to forget the filters.

Let me know if this approach sounds good to you, and I'll update the PR!

@dkotter

dkotter commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

That's a decently thought out approach and I'm not opposed to it. One other solution to consider after looking at things again is to add this filter in the existing ensure_*_supported methods, as I believe every Ability calls that and it's expected any new Ability added in the future will call that. So this wouldn't require a new method to be called and if we run the filter first and then run the supported check, we also ensure any changes someone makes to the prompt builder are supported by a configured model.

So I'd suggest still adding a new filter_prompt_builder method that runs the filter and then use that in the supported methods:

private function filter_prompt_builder( $prompt_builder ) {
      $ability_slug = $this->get_ability_slug();

      $filtered = apply_filters( "wpai_prompt_builder_{$ability_slug}", $prompt_builder, $this );

      // Guard against a filter returning a non-builder (null, WP_Error, etc.).
      if ( ! $filtered instanceof \WP_AI_Client_Prompt_Builder ) {
              return $prompt_builder;
      }

      return $filtered;
}

protected function ensure_text_generation_supported( $prompt_builder, string $message ) {
      $prompt_builder = $this->filter_prompt_builder( $prompt_builder );

      if ( ! $prompt_builder->is_supported_for_text_generation() ) {
              return new WP_Error( 'unsupported_model', $message );
      }

      return $prompt_builder;
}

@the-hercules

Copy link
Copy Markdown
Contributor Author

@dkotter
That’s a good idea to reduce boilerplate, the process of trying to handle the filter automatically inside ensure_*_supported().

However, I'd lean toward keeping a dedicated filter_prompt_builder() helper instead, for two main reasons:

1. Loss of Explicit Context Variables
Right now, some abilities pass highly useful, explicit variables to the filter. For example, Summarization passes the $length argument alongside the prompt:
apply_filters( "wpai_..._prompt_builder", $builder, $prompt, $length );

This allows a third-party developer to easily check if the requested summary is 'short' or 'long' and adjust the builder accordingly. If we bury the filter inside ensure_text_generation_supported( $builder, $message ), the hook loses access to those specific variables.

While we could pass $this, the Ability classes are stateless during a request (they don't store $length as a class property). This means a developer inspecting $this still cannot access the $length for the current execution. The only way to fix this would be to update the signature of ensure_text_generation_supported() to accept ...$filter_args—but if we do that, we have to manually update all 12 ability files to pass those arguments down anyway, which defeats the boilerplate-reduction goal of the suggestion!

2. Separation of Concerns
A method named ensure_*_supported strongly signals that it's strictly a validation check. Firing a powerful, mutating developer hook inside a validation method is a side-effect that might surprise future contributors. Keeping the hook in a dedicated filter_prompt_builder() helper makes the intent of the code much clearer.

Since new abilities will have to call $this->set_provider_model_preference() anyway, wrapping that inside a dedicated $this->filter_prompt_builder() still centralizes the logic and makes it very hard to forget.

Let me know what you think.

@dkotter

dkotter commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@the-hercules This all sounds like valid arguments to me so happy to review and test things once changes are up. Thanks!

 - Add ability-specific filters for system instructions, prompts, and prompt builders
- Refactor  to process dynamic  hooks
- Apply new extension points across all existing AI abilities
- Add  documentation and integration tests
@the-hercules
the-hercules requested a review from a team July 7, 2026 18:07
@the-hercules
the-hercules requested a review from dkotter July 7, 2026 18:33
@the-hercules

Copy link
Copy Markdown
Contributor Author

@dkotter I’ve committed the changes you requested. Could you please review them when you get a chance?

@dkotter
dkotter requested a review from jeffpaul as a code owner July 20, 2026 19:34
@dkotter dkotter modified the milestones: Future Release, 1.3.0 Jul 20, 2026
@dkotter
dkotter merged commit 1aabfe3 into WordPress:develop Jul 20, 2026
24 checks passed
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.

Add extension points for custom prompt templates

3 participants