-
Notifications
You must be signed in to change notification settings - Fork 141
[Feature]: Issue 45 | Content Classification (Contextual Tagging) #313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dkotter
merged 80 commits into
WordPress:develop
from
TylerB24890:feature/issue-45-contextual-tagging
Apr 6, 2026
Merged
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 5c4bbbe
Add styles to the Contextual Tagging editor UI elements
TylerB24890 4393d45
Update the button injection into the term panels. Ensure the suggesti…
TylerB24890 0e22a9e
Minor styling update to editor UI
TylerB24890 4d101fc
Use WP core style variables and a consistent suggestion pill UI
TylerB24890 fee5422
Use the editor.PostTaxonomyType filter to inject the contextual taggi…
TylerB24890 46d1502
use core wordcount package to count the post content words.
TylerB24890 f1d2e52
Use taxonomy object to retrieve labels.
TylerB24890 edc2ba6
Share constants and methods across objects. Avoid unnecessary queries…
TylerB24890 c1e472a
Add support for parent terms with suggestions
TylerB24890 ea7c4b5
Add filters to context passed to AI and returned suggestions
TylerB24890 72092be
Update and fix unit tests
TylerB24890 718addb
Create experiment documentation and README files
TylerB24890 fe8ca8e
Use the actual term name in the suggested terms output
TylerB24890 8bd73c6
Add wordcount package to dependencies list & fix function documentation
TylerB24890 6f39429
ESLint fixes
TylerB24890 26807f4
Remove redundant spinner & center align generate buttons
TylerB24890 7c4e01d
Add 'Suggested [Taxonomy]' heading to suggestion panel
TylerB24890 ec14a57
Update all filter prefixes to wpai. Replace @since tag with x.x.x pla…
TylerB24890 3b20735
Create system-instruction file separately and use structured output f…
TylerB24890 36ff747
Fix term saving
TylerB24890 caa5b4a
Merge latest develop and resolve conflicts
TylerB24890 41df338
Remove since tags on inheritDoc
TylerB24890 4e57da5
Update ability filter prefixes
TylerB24890 2f54ec7
Fix unit tests. Fix phpcs flags. Fix phpstan flags.
TylerB24890 5bd85e1
Clean up admin settings UI
TylerB24890 115ed91
Add taxonomy support check to the script localization
TylerB24890 2329f5b
Ensure currently assigned terms aren't returned as suggestions.
TylerB24890 ed9ab4c
Update regenerate label
TylerB24890 b2cb089
Register new Contextual Tagging experiment. Create editor UI and unit…
TylerB24890 b56a454
Add styles to the Contextual Tagging editor UI elements
TylerB24890 32855b5
Update the button injection into the term panels. Ensure the suggesti…
TylerB24890 a6378cc
Minor styling update to editor UI
TylerB24890 39e9e40
Use WP core style variables and a consistent suggestion pill UI
TylerB24890 934ed5d
Use the editor.PostTaxonomyType filter to inject the contextual taggi…
TylerB24890 5eb0919
use core wordcount package to count the post content words.
TylerB24890 3d6761b
Use taxonomy object to retrieve labels.
TylerB24890 56cddb4
Share constants and methods across objects. Avoid unnecessary queries…
TylerB24890 2703a33
Add support for parent terms with suggestions
TylerB24890 0a94a9c
Add filters to context passed to AI and returned suggestions
TylerB24890 bc4c319
Update and fix unit tests
TylerB24890 f3a4b88
Create experiment documentation and README files
TylerB24890 86893de
Use the actual term name in the suggested terms output
TylerB24890 84832d7
Add wordcount package to dependencies list & fix function documentation
TylerB24890 3f95850
ESLint fixes
TylerB24890 6bad811
Remove redundant spinner & center align generate buttons
TylerB24890 6c1a5ca
Add 'Suggested [Taxonomy]' heading to suggestion panel
TylerB24890 47ca4f6
Update all filter prefixes to wpai. Replace @since tag with x.x.x pla…
TylerB24890 eb36a5c
Create system-instruction file separately and use structured output f…
TylerB24890 e8f8221
Fix term saving
TylerB24890 bce1303
Remove since tags on inheritDoc
TylerB24890 989827d
Update ability filter prefixes
TylerB24890 4945847
Fix unit tests. Fix phpcs flags. Fix phpstan flags.
TylerB24890 7359364
Clean up admin settings UI
TylerB24890 a1e5e33
Add taxonomy support check to the script localization
TylerB24890 c7dab80
Ensure currently assigned terms aren't returned as suggestions.
TylerB24890 0340c41
Update regenerate label
TylerB24890 14b0fe3
Merge the latest develop and resolve conflicts
TylerB24890 f129b56
Remove existing terms from the prompt and only pass currently assigne…
TylerB24890 e0e8f48
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 550d10b
Rename Contextual Tagging to Content Classification. Update all docum…
TylerB24890 3aed367
JS lint fix
TylerB24890 53c012c
Add mock responses for the content-classification experiment
TylerB24890 342a3e6
Create content-classification E2E tests.
TylerB24890 6272364
Update includes/Abilities/Content_Classification/Content_Classificati…
TylerB24890 3b0070e
Update includes/Abilities/Content_Classification/Content_Classificati…
TylerB24890 282856b
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 ba24f33
Merge branch 'feature/issue-45-contextual-tagging' of github.com:Tyle…
TylerB24890 fb12ae8
Send the top 100 terms to the LLM when existing_only strategy is sele…
TylerB24890 24949c1
Add additional unit tests for patch coverage
TylerB24890 bbfc76f
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 af73b7c
Remove sanitize_callback parameters from abilities
TylerB24890 3d844a0
Add is_supported_* check for text generation support.
TylerB24890 950c93e
Update the system instruction to return terms in the same language as…
TylerB24890 3186dea
Update unit tests to match new method signatures
TylerB24890 979eb5b
Add unit tests for new methods and signatures.
TylerB24890 c9cc937
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 b9fae88
Fix openAI support
TylerB24890 13848fa
Fix parent terms being returned for non-hierarchical taxonomies. Fix …
TylerB24890 354fb82
Merge branch 'develop' into feature/issue-45-contextual-tagging
TylerB24890 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
590 changes: 590 additions & 0 deletions
590
includes/Abilities/Content_Classification/Content_Classification.php
Large diffs are not rendered by default.
Oops, something went wrong.
29 changes: 29 additions & 0 deletions
29
includes/Abilities/Content_Classification/system-instruction.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
| - 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
301
includes/Experiments/Content_Classification/Content_Classification.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 ) | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.