PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / includes / ai-copilot / analysis.php

analysis.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.8, at includes/ai-copilot/analysis.php

184 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — AI Copilot analysis: prompts, schemas, meta storage.
4 *
5 * This module owns:
6 * - The JSON Schema for comment analysis (filterable).
7 * - The prompt builder that converts a comment into a chat message array.
8 * - Meta read/write helpers so the job callback never touches meta keys
9 * directly; the key name lives in one place.
10 *
11 * Comment analysis is the only auto-analysis the copilot performs (it feeds
12 * the comments-window spam score). Posts, pages, and terms are not analyzed.
13 *
14 * Meta key: `_desktop_mode_ai_analysis` (prefixed underscore → hidden from
15 * the Custom Fields UI by default).
16 *
17 * @package OpenStation
18 */
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Meta key used to store the per-comment AI analysis.
24 *
25 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
26 * persisted or externally-visible identifier, so renaming it would
27 * orphan data already written by live installs (or break a live
28 * URL). The mismatch between this constant's name and its value is
29 * deliberate — it is NOT a half-finished rename.
30 */
31 const OPENSTATION_AI_META_KEY = '_desktop_mode_ai_analysis';
32
33 /** Max characters of comment text sent to the provider. */
34 const OPENSTATION_AI_CONTENT_MAX_CHARS = 3000;
35
36 // ---------------------------------------------------------------------------
37 // JSON Schemas
38 // ---------------------------------------------------------------------------
39
40 /**
41 * JSON Schema for comment analysis.
42 *
43 * Captures a topic label and summary plus the `harmful` and `spam`
44 * booleans that drive the comments-window spam score.
45 *
46 * @return array
47 */
48 function openstation_ai_schema_comment() {
49 $schema = array(
50 'type' => 'object',
51 'additionalProperties' => false,
52 'required' => array( 'topic', 'ai_summary', 'harmful', 'spam' ),
53 'properties' => array(
54 'topic' => array(
55 'type' => 'string',
56 'description' => 'A concise topic label (max 10 words) capturing the nature and tone of the comment. Include sentiment cues when relevant — e.g. "hostile criticism of article quality", "enthusiastic praise for travel tips", "spam promotion of hotel deals". This label is used by a search engine to match user queries like "negative comment" or "congratulatory message".',
57 ),
58 'ai_summary' => array(
59 'type' => 'string',
60 'description' => 'A 1-2 sentence summary that captures both WHAT the commenter said AND HOW they said it (tone, sentiment, intent). A negative or angry comment should be described as such. Examples: "The commenter aggressively dismisses the article as low quality and insults the author\'s credibility." / "A reader warmly congratulates the author on the new baby." / "A promotional spam comment linking to a hotel deals website with no relevance to the post."',
61 ),
62 'harmful' => array(
63 'type' => 'boolean',
64 'description' => 'True when the comment is hostile, insulting, demeaning, or abusive — regardless of whether it contains explicit language. Set to TRUE for: personal attacks on the author or other commenters ("garbage article", "you clearly have no idea", "embarrassing journalism", "stop writing about X"), aggressive condescension, hate speech, threats, or harassment. Set to FALSE for: polite disagreement, constructive criticism, or promotional spam that is off-topic but not hostile. Note: a comment can be spam=true AND harmful=false (promotional but not hostile), or harmful=true AND spam=false (angry but on-topic).',
65 ),
66 'spam' => array(
67 'type' => 'boolean',
68 'description' => 'True when the comment is promotional, automated, or wholly unrelated to the post content. Clear signals: external links to commercial sites (cheaphotelsnow.biz, etc.), ALL CAPS promotional text, trigger phrases like "CLICK HERE", "BOOK NOW", "AMAZING deals", "LIMITED TIME OFFER", excessive exclamation marks, generic praise unrelated to the post subject. Set to FALSE for comments that are angry, negative, or critical — those belong under `harmful`, not `spam`. A hostile but on-topic comment is NOT spam.',
69 ),
70 ),
71 );
72
73 /**
74 * Filters the JSON Schema used for comment AI analysis.
75 *
76 * @param array $schema The JSON Schema array.
77 */
78 return (array) apply_filters( 'openstation_ai_schema_comment', $schema );
79 }
80
81 // ---------------------------------------------------------------------------
82 // Prompt builders
83 // ---------------------------------------------------------------------------
84
85 /**
86 * Builds the messages array for a comment.
87 *
88 * The parent post's title is included so the model can judge `spam`
89 * (off-topic detection requires knowing what the post is about).
90 *
91 * @param WP_Comment $comment
92 * @return array Chat messages array.
93 */
94 function openstation_ai_messages_for_comment( WP_Comment $comment ) {
95 $text = wp_strip_all_tags( $comment->comment_content );
96 $text = mb_substr( preg_replace( '/\s+/', ' ', trim( $text ) ), 0, OPENSTATION_AI_CONTENT_MAX_CHARS );
97
98 $user_text = "Analyze the following WordPress comment.\n\n";
99 $user_text .= "Comment:\n{$text}\n\n";
100
101 // Give the model post context so it can evaluate relevance (spam).
102 $post_id = (int) $comment->comment_post_ID;
103 if ( $post_id > 0 ) {
104 $post = get_post( $post_id );
105 if ( $post instanceof WP_Post ) {
106 $user_text .= 'Post title: ' . wp_strip_all_tags( $post->post_title ) . "\n";
107 }
108 }
109
110 $user_text .= "\n\nClassification rules:\n";
111 $user_text .= "- `harmful = true`: the comment is hostile, insulting, or demeaning — e.g. attacks on the author's competence, aggressive rhetoric, threats, hate speech. Tone matters: an angry rant calling the article \"garbage\" is harmful even without explicit language.\n";
112 $user_text .= "- `spam = true`: the comment is promotional or off-topic — e.g. commercial links, ALL CAPS sales copy, \"CLICK HERE\" / \"BOOK NOW\", generic praise unrelated to the post.\n";
113 $user_text .= "- These are INDEPENDENT flags. A hostile but on-topic comment is harmful=true, spam=false. A promotional but politely worded comment is spam=true, harmful=false. Both can be true simultaneously.\n";
114 $user_text .= '- The `topic` and `ai_summary` fields MUST capture the tone and sentiment so that search queries like "negative comment", "angry reader", or "spam" return the correct results.';
115
116 /**
117 * Filters the user message sent to the provider for comment analysis.
118 *
119 * @param string $user_text The composed user message.
120 * @param WP_Comment $comment The comment being analyzed.
121 */
122 $user_text = (string) apply_filters( 'openstation_ai_comment_prompt', $user_text, $comment );
123
124 return array(
125 array(
126 'role' => 'system',
127 'content' => 'You are a content moderation assistant for a WordPress site. Your analysis is used by a semantic search engine, so the topic label and summary must reflect the comment\'s TONE and SENTIMENT — not just its subject matter. An angry, insulting comment must be described as angry and insulting. A promotional spam comment must be described as promotional spam. A warm congratulatory message must be described as warm and positive. Accurate tone labelling is critical for search to work.',
128 ),
129 array(
130 'role' => 'user',
131 'content' => $user_text,
132 ),
133 );
134 }
135
136 // ---------------------------------------------------------------------------
137 // Meta read / write
138 // ---------------------------------------------------------------------------
139
140 /**
141 * Saves an AI analysis result as comment meta.
142 *
143 * Comments are the only entity the copilot analyzes. The `$entity_type`
144 * parameter is retained for call-site/signature stability but only
145 * `'comment'` is supported — any other value is a no-op that returns false.
146 *
147 * @param string $entity_type Only `'comment'` is supported.
148 * @param int $entity_id Comment ID.
149 * @param array $analysis The structured output array from the provider.
150 * @return bool
151 */
152 function openstation_ai_save_meta( $entity_type, $entity_id, array $analysis ) {
153 $entity_id = (int) $entity_id;
154 if ( $entity_id <= 0 || 'comment' !== $entity_type ) {
155 return false;
156 }
157
158 // Stamp when the analysis was performed so consumers can detect staleness.
159 $analysis['analyzed_at'] = time();
160
161 return false !== update_comment_meta( $entity_id, OPENSTATION_AI_META_KEY, $analysis );
162 }
163
164 /**
165 * Retrieves a previously saved comment AI analysis, or null if none exists.
166 *
167 * Only `'comment'` is supported (see {@see openstation_ai_save_meta}); any
168 * other `$entity_type` returns null.
169 *
170 * @param string $entity_type Only `'comment'` is supported.
171 * @param int $entity_id Comment ID.
172 * @return array|null
173 */
174 function openstation_ai_get_meta( $entity_type, $entity_id ) {
175 $entity_id = (int) $entity_id;
176 if ( $entity_id <= 0 || 'comment' !== $entity_type ) {
177 return null;
178 }
179
180 $raw = get_comment_meta( $entity_id, OPENSTATION_AI_META_KEY, true );
181
182 return is_array( $raw ) && ! empty( $raw ) ? $raw : null;
183 }
184