Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
837f04f
Register new Contextual Tagging experiment. Create editor UI and unit…
TylerB24890 Mar 17, 2026
5c4bbbe
Add styles to the Contextual Tagging editor UI elements
TylerB24890 Mar 17, 2026
4393d45
Update the button injection into the term panels. Ensure the suggesti…
TylerB24890 Mar 17, 2026
0e22a9e
Minor styling update to editor UI
TylerB24890 Mar 17, 2026
4d101fc
Use WP core style variables and a consistent suggestion pill UI
TylerB24890 Mar 17, 2026
fee5422
Use the editor.PostTaxonomyType filter to inject the contextual taggi…
TylerB24890 Mar 17, 2026
46d1502
use core wordcount package to count the post content words.
TylerB24890 Mar 17, 2026
f1d2e52
Use taxonomy object to retrieve labels.
TylerB24890 Mar 17, 2026
edc2ba6
Share constants and methods across objects. Avoid unnecessary queries…
TylerB24890 Mar 17, 2026
c1e472a
Add support for parent terms with suggestions
TylerB24890 Mar 17, 2026
ea7c4b5
Add filters to context passed to AI and returned suggestions
TylerB24890 Mar 17, 2026
72092be
Update and fix unit tests
TylerB24890 Mar 17, 2026
718addb
Create experiment documentation and README files
TylerB24890 Mar 17, 2026
fe8ca8e
Use the actual term name in the suggested terms output
TylerB24890 Mar 17, 2026
8bd73c6
Add wordcount package to dependencies list & fix function documentation
TylerB24890 Mar 18, 2026
6f39429
ESLint fixes
TylerB24890 Mar 18, 2026
26807f4
Remove redundant spinner & center align generate buttons
TylerB24890 Mar 18, 2026
7c4e01d
Add 'Suggested [Taxonomy]' heading to suggestion panel
TylerB24890 Mar 18, 2026
ec14a57
Update all filter prefixes to wpai. Replace @since tag with x.x.x pla…
TylerB24890 Mar 19, 2026
3b20735
Create system-instruction file separately and use structured output f…
TylerB24890 Mar 19, 2026
36ff747
Fix term saving
TylerB24890 Mar 19, 2026
caa5b4a
Merge latest develop and resolve conflicts
TylerB24890 Mar 19, 2026
41df338
Remove since tags on inheritDoc
TylerB24890 Mar 19, 2026
4e57da5
Update ability filter prefixes
TylerB24890 Mar 19, 2026
2f54ec7
Fix unit tests. Fix phpcs flags. Fix phpstan flags.
TylerB24890 Mar 19, 2026
5bd85e1
Clean up admin settings UI
TylerB24890 Mar 19, 2026
115ed91
Add taxonomy support check to the script localization
TylerB24890 Mar 19, 2026
2329f5b
Ensure currently assigned terms aren't returned as suggestions.
TylerB24890 Mar 19, 2026
ed9ab4c
Update regenerate label
TylerB24890 Mar 19, 2026
b2cb089
Register new Contextual Tagging experiment. Create editor UI and unit…
TylerB24890 Mar 17, 2026
b56a454
Add styles to the Contextual Tagging editor UI elements
TylerB24890 Mar 17, 2026
32855b5
Update the button injection into the term panels. Ensure the suggesti…
TylerB24890 Mar 17, 2026
a6378cc
Minor styling update to editor UI
TylerB24890 Mar 17, 2026
39e9e40
Use WP core style variables and a consistent suggestion pill UI
TylerB24890 Mar 17, 2026
934ed5d
Use the editor.PostTaxonomyType filter to inject the contextual taggi…
TylerB24890 Mar 17, 2026
5eb0919
use core wordcount package to count the post content words.
TylerB24890 Mar 17, 2026
3d6761b
Use taxonomy object to retrieve labels.
TylerB24890 Mar 17, 2026
56cddb4
Share constants and methods across objects. Avoid unnecessary queries…
TylerB24890 Mar 17, 2026
2703a33
Add support for parent terms with suggestions
TylerB24890 Mar 17, 2026
0a94a9c
Add filters to context passed to AI and returned suggestions
TylerB24890 Mar 17, 2026
bc4c319
Update and fix unit tests
TylerB24890 Mar 17, 2026
f3a4b88
Create experiment documentation and README files
TylerB24890 Mar 17, 2026
86893de
Use the actual term name in the suggested terms output
TylerB24890 Mar 17, 2026
84832d7
Add wordcount package to dependencies list & fix function documentation
TylerB24890 Mar 18, 2026
3f95850
ESLint fixes
TylerB24890 Mar 18, 2026
6bad811
Remove redundant spinner & center align generate buttons
TylerB24890 Mar 18, 2026
6c1a5ca
Add 'Suggested [Taxonomy]' heading to suggestion panel
TylerB24890 Mar 18, 2026
47ca4f6
Update all filter prefixes to wpai. Replace @since tag with x.x.x pla…
TylerB24890 Mar 19, 2026
eb36a5c
Create system-instruction file separately and use structured output f…
TylerB24890 Mar 19, 2026
e8f8221
Fix term saving
TylerB24890 Mar 19, 2026
bce1303
Remove since tags on inheritDoc
TylerB24890 Mar 19, 2026
989827d
Update ability filter prefixes
TylerB24890 Mar 19, 2026
4945847
Fix unit tests. Fix phpcs flags. Fix phpstan flags.
TylerB24890 Mar 19, 2026
7359364
Clean up admin settings UI
TylerB24890 Mar 19, 2026
a1e5e33
Add taxonomy support check to the script localization
TylerB24890 Mar 19, 2026
c7dab80
Ensure currently assigned terms aren't returned as suggestions.
TylerB24890 Mar 19, 2026
0340c41
Update regenerate label
TylerB24890 Mar 19, 2026
14b0fe3
Merge the latest develop and resolve conflicts
TylerB24890 Mar 20, 2026
f129b56
Remove existing terms from the prompt and only pass currently assigne…
TylerB24890 Mar 20, 2026
e0e8f48
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 Mar 23, 2026
550d10b
Rename Contextual Tagging to Content Classification. Update all docum…
TylerB24890 Mar 25, 2026
3aed367
JS lint fix
TylerB24890 Mar 25, 2026
53c012c
Add mock responses for the content-classification experiment
TylerB24890 Mar 25, 2026
342a3e6
Create content-classification E2E tests.
TylerB24890 Mar 25, 2026
6272364
Update includes/Abilities/Content_Classification/Content_Classificati…
TylerB24890 Apr 2, 2026
3b0070e
Update includes/Abilities/Content_Classification/Content_Classificati…
TylerB24890 Apr 2, 2026
282856b
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 Apr 2, 2026
ba24f33
Merge branch 'feature/issue-45-contextual-tagging' of github.com:Tyle…
TylerB24890 Apr 2, 2026
fb12ae8
Send the top 100 terms to the LLM when existing_only strategy is sele…
TylerB24890 Apr 2, 2026
24949c1
Add additional unit tests for patch coverage
TylerB24890 Apr 2, 2026
bbfc76f
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 Apr 3, 2026
af73b7c
Remove sanitize_callback parameters from abilities
TylerB24890 Apr 3, 2026
3d844a0
Add is_supported_* check for text generation support.
TylerB24890 Apr 3, 2026
950c93e
Update the system instruction to return terms in the same language as…
TylerB24890 Apr 3, 2026
3186dea
Update unit tests to match new method signatures
TylerB24890 Apr 3, 2026
979eb5b
Add unit tests for new methods and signatures.
TylerB24890 Apr 3, 2026
c9cc937
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 Apr 3, 2026
b9fae88
Fix openAI support
TylerB24890 Apr 3, 2026
13848fa
Fix parent terms being returned for non-hierarchical taxonomies. Fix …
TylerB24890 Apr 6, 2026
354fb82
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 Apr 6, 2026
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
402 changes: 402 additions & 0 deletions docs/experiments/content-classification.md

Large diffs are not rendered by default.

590 changes: 590 additions & 0 deletions includes/Abilities/Content_Classification/Content_Classification.php

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions includes/Abilities/Content_Classification/system-instruction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/**
* System instruction for the Content Classification ability.
*
* @package WordPress\AI\Abilities\Content_Classification
*/

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// phpcs:ignore Squiz.PHP.Heredoc.NotAllowed, PluginCheck.CodeAnalysis.Heredoc.NotAllowed
return <<<'INSTRUCTION'
You are a content taxonomy assistant for a WordPress website. Your task is to analyze article content and suggest relevant taxonomy terms.

Goal: Analyze the provided content (wrapped in <content> tags, with any already-applied terms in <assigned-terms>) and suggest relevant terms for the taxonomy (wrapped in <taxonomy> tag).

Rules:
- The taxonomy to suggest terms for is wrapped in the <taxonomy> tag.
- Suggest as many relevant terms as you can identify from the content.
- The "term" field must contain ONLY the human-readable tag or category name (1-3 words), in Title Case (e.g., "Machine Learning", not "machine learning").
- Confidence should reflect relevance: 1.0 = perfect match, 0.5 = somewhat relevant. Only suggest terms with confidence >= 0.5.
- Do not suggest duplicate or near-duplicate terms.
- Do not suggest terms listed in <assigned-terms> — they are already applied to this post.
- When the <available-terms> tag is provided, strongly prefer selecting from those terms.
- Prioritize specificity and relevance over breadth.
Comment thread
TylerB24890 marked this conversation as resolved.
- Ensure the terms you suggest match the language of the content you are given. For example, if the content is in English, suggest English terms. If the content is in Spanish, suggest Spanish terms.
INSTRUCTION;
301 changes: 301 additions & 0 deletions includes/Experiments/Content_Classification/Content_Classification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
<?php
/**
* Content classification experiment implementation.
*
* @package WordPress\AI
*/

declare( strict_types=1 );

namespace WordPress\AI\Experiments\Content_Classification;

use WordPress\AI\Abilities\Content_Classification\Content_Classification as Content_Classification_Ability;
use WordPress\AI\Abstracts\Abstract_Feature;
use WordPress\AI\Asset_Loader;
use WordPress\AI\Experiments\Experiment_Category;
use WordPress\AI\Settings\Settings_Registration;

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Content classification experiment.
*
* Provides AI-powered suggestions for post taxonomies
* based on a comprehensive analysis of the post content.
*
* @since x.x.x
*/
class Content_Classification extends Abstract_Feature {

/**
* The default taxonomy strategy.
*
* @since x.x.x
*
* @var string
*/
public const STRATEGY_EXISTING_ONLY = 'existing_only';

/**
* The strategy that allows new term suggestions.
*
* @since x.x.x
*
* @var string
*/
public const STRATEGY_ALLOW_NEW = 'allow_new';

/**
* The default maximum number of suggestions.
*
* @since x.x.x
*
* @var int
*/
public const DEFAULT_MAX_SUGGESTIONS = 5;

/**
* {@inheritDoc}
*/
public static function get_id(): string {
return 'content-classification';
}

/**
* {@inheritDoc}
*/
protected function load_metadata(): array {
return array(
'label' => __( 'Content Classification', 'ai' ),
'description' => __( 'AI-powered suggestions for post tags and categories based on content analysis.', 'ai' ),
'category' => Experiment_Category::EDITOR,
);
}

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

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

/**
* Enqueues and localizes the admin script.
*
* @since x.x.x
*
* @param string $hook_suffix The current admin page hook suffix.
*/
public function enqueue_assets( string $hook_suffix ): void {
// Load asset in new post and edit post screens only.
if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix ) {
return;
}

$screen = get_current_screen();

// Load the assets only if the post type supports categories or tags and is not an attachment.
// Also check if the user can manage categories.
if (
! $screen ||
! current_user_can( 'manage_categories' ) ||
in_array( $screen->post_type, array( 'attachment' ), true ) ||
(
! is_object_in_taxonomy( $screen->post_type, 'category' ) &&
! is_object_in_taxonomy( $screen->post_type, 'post_tag' )
)
) {
return;
}

Asset_Loader::enqueue_script( 'content_classification', 'experiments/content-classification' );
Asset_Loader::enqueue_style( 'content_classification', 'experiments/content-classification' );
Asset_Loader::localize_script(
'content_classification',
'ContentClassificationData',
array(
'enabled' => $this->is_enabled(),
'strategy' => $this->get_strategy(),
'maxSuggestions' => $this->get_max_suggestions(),
)
);
}

/**
* Registers experiment-specific settings.
*
* @since x.x.x
*/
public function register_settings(): void {
register_setting(
Settings_Registration::OPTION_GROUP,
$this->get_field_option_name( 'strategy' ),
array(
'type' => 'string',
'default' => self::STRATEGY_EXISTING_ONLY,
'sanitize_callback' => array( $this, 'sanitize_strategy' ),
)
);

register_setting(
Settings_Registration::OPTION_GROUP,
$this->get_field_option_name( 'max_suggestions' ),
array(
'type' => 'integer',
'default' => self::DEFAULT_MAX_SUGGESTIONS,
'sanitize_callback' => array( $this, 'sanitize_max_suggestions' ),
)
);
}

/**
* Renders experiment-specific settings fields.
*
* @since x.x.x
*/
public function render_settings_fields(): void {
$strategy_option = $this->get_field_option_name( 'strategy' );
$max_suggestions_option = $this->get_field_option_name( 'max_suggestions' );
$current_strategy = get_option( $this->get_field_option_name( 'strategy' ), self::STRATEGY_EXISTING_ONLY );
$current_max = get_option( $this->get_field_option_name( 'max_suggestions' ), self::DEFAULT_MAX_SUGGESTIONS );
?>
<fieldset class="ai-experiments__item-fields">
<legend class="screen-reader-text"><?php esc_html_e( 'Content Classification Settings', 'ai' ); ?></legend>
<table class="ai-experiments__settings-table" role="presentation">
<tr>
<td>
<label for="<?php echo esc_attr( $strategy_option ); ?>">
<?php esc_html_e( 'Taxonomy strategy:', 'ai' ); ?>
</label>
</td>
<td>
<select
id="<?php echo esc_attr( $strategy_option ); ?>"
name="<?php echo esc_attr( $strategy_option ); ?>"
>
<option value="<?php echo esc_attr( self::STRATEGY_EXISTING_ONLY ); ?>" <?php selected( $current_strategy, self::STRATEGY_EXISTING_ONLY ); ?>>
<?php esc_html_e( 'Only suggest existing terms', 'ai' ); ?>
</option>
<option value="<?php echo esc_attr( self::STRATEGY_ALLOW_NEW ); ?>" <?php selected( $current_strategy, self::STRATEGY_ALLOW_NEW ); ?>>
<?php esc_html_e( 'Suggest new terms based on context', 'ai' ); ?>
</option>
</select>
</td>
</tr>
<tr>
<td>
<label for="<?php echo esc_attr( $max_suggestions_option ); ?>">
<?php esc_html_e( 'Maximum suggestions:', 'ai' ); ?>
</label>
</td>
<td>
<input
type="number"
id="<?php echo esc_attr( $max_suggestions_option ); ?>"
name="<?php echo esc_attr( $max_suggestions_option ); ?>"
value="<?php echo esc_attr( (string) $current_max ); ?>"
min="1"
max="10"
step="1"
/>
</td>
</tr>
</table>
</fieldset>
<?php
}

/**
* Sanitizes the strategy setting.
*
* @since x.x.x
*
* @param mixed $value The value to sanitize.
* @return string The sanitized strategy value.
*/
public function sanitize_strategy( $value ): string {
$valid = array( self::STRATEGY_EXISTING_ONLY, self::STRATEGY_ALLOW_NEW );

return in_array( $value, $valid, true ) ? $value : self::STRATEGY_EXISTING_ONLY;
}

/**
* Sanitizes the max suggestions setting.
*
* @since x.x.x
*
* @param mixed $value The value to sanitize.
* @return int The sanitized max suggestions value.
*/
public function sanitize_max_suggestions( $value ): int {
$value = absint( $value );

return max( 1, min( 10, $value ) );
}

/**
* Gets the strategy to use for content classification.
*
* @since x.x.x
*
* @return string The strategy to use.
*/
public function get_strategy(): string {
$strategy = get_option( $this->get_field_option_name( 'strategy' ), self::STRATEGY_EXISTING_ONLY );

/**
* Filters the strategy to use for content classification.
*
* @since x.x.x
*
* @param string $strategy The strategy to use.
* @return string The filtered strategy.
*/
$strategy = apply_filters( 'wpai_content_classification_strategy', $strategy );

// Return the sanitized strategy value.
return $this->sanitize_strategy( $strategy );
}

/**
* Gets the maximum number of suggestions to generate for content classification.
*
* @since x.x.x
*
* @return int The maximum number of suggestions to generate.
*/
public function get_max_suggestions(): int {
$max_suggestions = (int) get_option( $this->get_field_option_name( 'max_suggestions' ), self::DEFAULT_MAX_SUGGESTIONS );

/**
* Filters the maximum number of suggestions to generate for content classification.
*
* @since x.x.x
*
* @param int $max_suggestions The maximum number of suggestions to generate.
* @return int The filtered max suggestions.
*/
return $this->sanitize_max_suggestions(
apply_filters( 'wpai_content_classification_max_suggestions', $max_suggestions )
);
}
}
1 change: 1 addition & 0 deletions includes/Experiments/Experiments.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ final class Experiments {
*/
private const EXPERIMENT_CLASSES = array( // phpcs:ignore SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition -- This is used as an array const.
\WordPress\AI\Experiments\Abilities_Explorer\Abilities_Explorer::class,
\WordPress\AI\Experiments\Content_Classification\Content_Classification::class,
\WordPress\AI\Experiments\Excerpt_Generation\Excerpt_Generation::class,
\WordPress\AI\Experiments\Alt_Text_Generation\Alt_Text_Generation::class,
\WordPress\AI\Experiments\Image_Generation\Image_Generation::class,
Expand Down
7 changes: 4 additions & 3 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@wordpress/notices": "^5.35.0",
"@wordpress/plugins": "^7.34.0",
"@wordpress/url": "^4.38.0",
"@wordpress/wordcount": "^4.41.0",
"react": "^18.3.1"
},
"overrides": {
Expand Down
18 changes: 18 additions & 0 deletions src/admin/settings/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,22 @@
margin-left: 1.5rem;
}
}

/* Element: settings table (two-column label/field layout) */
&__settings-table {
border-spacing: 0 0.5rem;

td:first-child {
padding-right: 1rem;
white-space: nowrap;
}

label {
font-weight: 600;
}

input[type="number"] {
padding-right: 0;
}
}
}
Loading
Loading