Feat 514: add comment value score#681
Conversation
…in admin dashboard
…y logic on JS to update te status
|
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 Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @priyanshuhaldar007. 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. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
dkotter
left a comment
There was a problem hiding this comment.
Left a number of comments and also seeing quite a few lint failures that need fixed here. In addition, we should look to add both unit tests and update our E2E tests to ensure these changes are covered
| * @param string $post_id The ID of the post. | ||
| * @return array{toxicity_score: float, sentiment: string, value_score: float|null}|\WP_Error The analysis result. | ||
| */ | ||
| private function analyze_comment( string $content, string $author ) { | ||
| private function analyze_comment( string $content, string $author, string $post_id ) { |
There was a problem hiding this comment.
I believe $post_id should always be an integer
| * @param string $post_id The ID of the post. | |
| * @return array{toxicity_score: float, sentiment: string, value_score: float|null}|\WP_Error The analysis result. | |
| */ | |
| private function analyze_comment( string $content, string $author ) { | |
| private function analyze_comment( string $content, string $author, string $post_id ) { | |
| * @param int $post_id The ID of the post. | |
| * @return array{toxicity_score: float, sentiment: string, value_score: float|null}|\WP_Error The analysis result. | |
| */ | |
| private function analyze_comment( string $content, string $author, int $post_id ) { |
| * @param array{toxicity_score: float, sentiment: string, value_score: float|null}|null $result Precomputed analysis result. | ||
| * @param string $content Comment content. | ||
| * @param string $author Comment author name. | ||
| * @param string $post_id The ID of the post. |
There was a problem hiding this comment.
| * @param string $post_id The ID of the post. | |
| * @param int $post_id The ID of the post. |
| 'description' => esc_html__( 'The sentiment of the comment.', 'ai' ), | ||
| ), | ||
| 'value_score' => array( | ||
| 'type' => array( 'number', 'null' ), |
There was a problem hiding this comment.
Is there value in having this return null? I think I'd just match what we do for toxicity scoring and have a number between 0 and 1
| /** | ||
| * Gets the configuration for Value_Score levels. | ||
| * | ||
| * @since 1.0.0 |
There was a problem hiding this comment.
| * @since 1.0.0 | |
| * @since x.x.x |
| /** | ||
| * Renders the value score column content. | ||
| * | ||
| * @since 1.0.0 |
There was a problem hiding this comment.
| * @since 1.0.0 | |
| * @since x.x.x |
| * @param int $comment_id The comment ID. | ||
| * @param string $status The analysis status. | ||
| */ | ||
| private function render_value_score_column( int $comment_id, string $status ): void { |
There was a problem hiding this comment.
Similar here, this method is basically the same as render_toxicity_column and render_sentiment_column. I'd suggest we introduce a new generic method that can be used by all three of these to avoid duplicate code
| * @return string The content of the post. | ||
| */ | ||
| private function get_post_context( int $post_id ): ?string { | ||
| $post = get_post( $post_id ); |
There was a problem hiding this comment.
The post may not exist so we need an early return check here
| $post = get_post( $post_id ); | ||
|
|
||
| // 1. Use excerpt if available (human-written, most reliable) | ||
| $excerpt = trim( $post->post_excerpt ); |
There was a problem hiding this comment.
Any benefit to using the excerpt or summary over just using the full post content?
| return null; | ||
| } | ||
|
|
||
| return mb_substr( $content, 0, 650 ); |
There was a problem hiding this comment.
Not sure we need to trim this. I understand it will save tokens but likely gives us worse results
| $post_context = $this->get_post_context( absint( $post_id ) ); | ||
|
|
||
| $prompt = sprintf( | ||
| "Comment by %s:\n\"\"\"%s\"\"\"", | ||
| "Comment by %s:\n\"\"\"%s\"\"\"\nContext:\n\"\"\"%s\"\"\"", | ||
| $author, | ||
| $content | ||
| $content, | ||
| $post_context | ||
| ); |
There was a problem hiding this comment.
Now that we're passing in the content of the post a comment is on, I'd suggest we change how we build this to send to the LLM. You can look at our other abilities to see how they work but generally we put things in XML-like tags
What?
Closes #514
Adds a Value Score column to the comment moderation list table. Comments are now analyzed for how relevant and valuable they are to the article they're posted on, in addition to the existing toxicity and sentiment signals.
Why?
Toxicity and sentiment alone don't tell the full story of a comment's quality. A comment can be perfectly polite but still be spam, a generic "+1", or completely off-topic. This PR introduces a value score (0–1) so moderators can quickly identify substantive, on-topic contributions versus low-effort noise — making triage faster and more informed.
How?
The implementation follows the exact same pattern as the existing
toxicity_scorefield end-to-end:AI / Prompt layer
system-instruction.phpto instruct the model to return a third field,value_score, scored 0–1 with clear band definitions (low/medium/high). The prompt now also passes post context (excerpt → AI summary → trimmed content fallback) so the model can actually assess relevance against the article.get_post_context()toComment_Analysis.phpto fetch and prepare that context, with a graceful 650-char truncation fallback on raw content.analyze_comment()now accepts and passes$post_idthrough to the prompt builder.Schema & storage
output_schema()andresponse_schema()both declarevalue_scoreas a nullable float (0–1). It's nullable for cases where the post content is unavailable or too sparse to judge relevance.sanitize_analysis_result()clamps the value to [0, 1] and preservesnull.META_VALUE_SCORE(_wpai_value_score) and returned in the ability response payload.Comment Moderation UI (PHP)
VALUE_SCORE_LOW / MEDIUM / HIGHconstants andget_value_score_config()with the same range-bucket shape used by toxicity, so the frontend JS can resolve badges identically.wpai_value_scorecolumn registered inadd_columns()andadd_sortable_columns().render_value_score_column()andrender_value_score_badge()added, mirroring the toxicity equivalents.handle_sorting_and_filtering()extended to supportwpai_value_scoreordering via a meta query, same pattern as toxicity sorting.enqueue_assets()now passesvalue_scorelabel config intowindow.aiCommentModerationData.labels.ai-badge--high-value,ai-badge--medium-value, andai-badge--low-valueto the existing selectors — no new colour definitions needed.Frontend JS / TSX
AnalysisResulttype extended withvalue_score: number.Windowdeclaration extended with thevalue_scorelabels shape.PendingCommenttype extended withvalueScoreBadge: HTMLElement.getValueScoreDisplay()helper added alongsidegetToxicityDisplay(), using the same range-bucket lookup.updateBadges(),findPendingComments(),analyzeComment()all updated to handle the third badge — detection, processing state, result rendering, and failure state.Use of AI Tools
AI assistance: Yes
Tool(s): Claude
Model(s): Claude Sonnet 4.6
Used for: Drafting this PR description from the git diff. All code was written and reviewed by me, with some contributions from the copilot for updating and generating doc blocks
Testing Instructions
Screenshots or screencast
Changelog Entry