PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.8
1.1.10 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 All 34 releases
desktop-mode / includes / ai-copilot / analysis.php

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

336 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — AI Copilot analysis: prompts, schemas, meta storage.
4 *
5 * This module owns:
6 * - JSON Schema definitions for each entity type (filterable).
7 * - Prompt builders that convert WP entities into chat message arrays.
8 * - Meta read/write helpers so job callbacks never touch meta keys
9 * directly; the key names live in one place.
10 *
11 * Meta key used for all entity types: `_desktop_mode_ai_analysis` (prefixed
12 * underscore → hidden from the Custom Fields UI by default).
13 *
14 * @package WPDesktopMode
15 */
16
17 defined( 'ABSPATH' ) || exit;
18
19 /** Meta key used across posts, terms, and comments. */
20 const DESKTOP_MODE_AI_META_KEY = '_desktop_mode_ai_analysis';
21
22 /** Max characters of post content / comment text sent to OpenAI. */
23 const DESKTOP_MODE_AI_CONTENT_MAX_CHARS = 3000;
24
25 // ---------------------------------------------------------------------------
26 // JSON Schemas
27 // ---------------------------------------------------------------------------
28
29 /**
30 * JSON Schema for post/page/term analysis.
31 *
32 * OpenAI strict mode requires `additionalProperties: false` at every level
33 * and all properties listed in `required`.
34 *
35 * @since 0.14.0
36 *
37 * @return array
38 */
39 function desktop_mode_ai_schema_content() {
40 $schema = array(
41 'type' => 'object',
42 'additionalProperties' => false,
43 'required' => array( 'topic', 'ai_summary' ),
44 'properties' => array(
45 'topic' => array(
46 'type' => 'string',
47 'description' => 'A concise topic label (max 10 words) capturing the main subject.',
48 ),
49 'ai_summary' => array(
50 'type' => 'string',
51 'description' => 'A 2-3 sentence summary of the content, written in plain language.',
52 ),
53 ),
54 );
55
56 /**
57 * Filters the JSON Schema used for post/page/term AI analysis.
58 *
59 * Must comply with OpenAI strict JSON Schema rules: every object level
60 * needs `additionalProperties: false` and all property names listed in
61 * `required`. Changing the shape here also requires updating any JS or
62 * PHP code that reads `_desktop_mode_ai_analysis` meta.
63 *
64 * @since 0.14.0
65 *
66 * @param array $schema The JSON Schema array.
67 */
68 return (array) apply_filters( 'desktop_mode_ai_schema_content', $schema );
69 }
70
71 /**
72 * JSON Schema for comment analysis.
73 *
74 * Extends the content schema with `harmful` and `spam` booleans.
75 *
76 * @since 0.14.0
77 *
78 * @return array
79 */
80 function desktop_mode_ai_schema_comment() {
81 $schema = array(
82 'type' => 'object',
83 'additionalProperties' => false,
84 'required' => array( 'topic', 'ai_summary', 'harmful', 'spam' ),
85 'properties' => array(
86 'topic' => array(
87 'type' => 'string',
88 '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".',
89 ),
90 'ai_summary' => array(
91 'type' => 'string',
92 '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."',
93 ),
94 'harmful' => array(
95 'type' => 'boolean',
96 '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).',
97 ),
98 'spam' => array(
99 'type' => 'boolean',
100 '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.',
101 ),
102 ),
103 );
104
105 /**
106 * Filters the JSON Schema used for comment AI analysis.
107 *
108 * @since 0.14.0
109 *
110 * @param array $schema The JSON Schema array.
111 */
112 return (array) apply_filters( 'desktop_mode_ai_schema_comment', $schema );
113 }
114
115 // ---------------------------------------------------------------------------
116 // Prompt builders
117 // ---------------------------------------------------------------------------
118
119 /**
120 * Builds the messages array for a post or page.
121 *
122 * @since 0.14.0
123 *
124 * @param WP_Post $post
125 * @return array Chat messages array.
126 */
127 function desktop_mode_ai_messages_for_post( WP_Post $post ) {
128 $type = ucfirst( $post->post_type );
129 $title = wp_strip_all_tags( $post->post_title );
130 $content = wp_strip_all_tags( $post->post_content );
131 $content = preg_replace( '/\s+/', ' ', trim( $content ) );
132 $content = mb_substr( $content, 0, DESKTOP_MODE_AI_CONTENT_MAX_CHARS );
133 $excerpt = wp_strip_all_tags( $post->post_excerpt );
134
135 $user_text = "Analyze the following WordPress {$type}.\n\n";
136 $user_text .= "Title: {$title}\n";
137 if ( $excerpt ) {
138 $user_text .= "Excerpt: {$excerpt}\n";
139 }
140 $user_text .= "Content:\n{$content}";
141
142 /**
143 * Filters the user message sent to OpenAI for post/page analysis.
144 *
145 * @since 0.14.0
146 *
147 * @param string $user_text The composed user message.
148 * @param WP_Post $post The post being analyzed.
149 */
150 $user_text = (string) apply_filters( 'desktop_mode_ai_post_prompt', $user_text, $post );
151
152 return array(
153 array(
154 'role' => 'system',
155 'content' => 'You are a content analysis assistant for a WordPress site. Analyze the provided content objectively and return structured data exactly matching the required schema.',
156 ),
157 array(
158 'role' => 'user',
159 'content' => $user_text,
160 ),
161 );
162 }
163
164 /**
165 * Builds the messages array for a taxonomy term (category, tag, etc.).
166 *
167 * @since 0.14.0
168 *
169 * @param WP_Term $term
170 * @return array Chat messages array.
171 */
172 function desktop_mode_ai_messages_for_term( WP_Term $term ) {
173 $taxonomy = ucwords( str_replace( '_', ' ', $term->taxonomy ) );
174 $name = $term->name;
175 $description = wp_strip_all_tags( $term->description );
176 $description = mb_substr( preg_replace( '/\s+/', ' ', trim( $description ) ), 0, DESKTOP_MODE_AI_CONTENT_MAX_CHARS );
177
178 $user_text = "Analyze the following WordPress {$taxonomy} term.\n\n";
179 $user_text .= "Name: {$name}\n";
180 if ( $description ) {
181 $user_text .= "Description: {$description}";
182 } else {
183 $user_text .= 'No description provided. Base your analysis on the term name alone.';
184 }
185
186 /**
187 * Filters the user message sent to OpenAI for term analysis.
188 *
189 * @since 0.14.0
190 *
191 * @param string $user_text The composed user message.
192 * @param WP_Term $term The term being analyzed.
193 */
194 $user_text = (string) apply_filters( 'desktop_mode_ai_term_prompt', $user_text, $term );
195
196 return array(
197 array(
198 'role' => 'system',
199 'content' => 'You are a content analysis assistant for a WordPress site. Analyze the provided taxonomy term and return structured data exactly matching the required schema.',
200 ),
201 array(
202 'role' => 'user',
203 'content' => $user_text,
204 ),
205 );
206 }
207
208 /**
209 * Builds the messages array for a comment.
210 *
211 * When the parent post has an existing AI analysis, its summary is
212 * included so the model can accurately judge `spam` (off-topic detection
213 * requires knowing what the post is about).
214 *
215 * @since 0.14.0
216 *
217 * @param WP_Comment $comment
218 * @return array Chat messages array.
219 */
220 function desktop_mode_ai_messages_for_comment( WP_Comment $comment ) {
221 $text = wp_strip_all_tags( $comment->comment_content );
222 $text = mb_substr( preg_replace( '/\s+/', ' ', trim( $text ) ), 0, DESKTOP_MODE_AI_CONTENT_MAX_CHARS );
223
224 $user_text = "Analyze the following WordPress comment.\n\n";
225 $user_text .= "Comment:\n{$text}\n\n";
226
227 // Give the model post context so it can evaluate relevance (spam).
228 $post_id = (int) $comment->comment_post_ID;
229 if ( $post_id > 0 ) {
230 $post = get_post( $post_id );
231 if ( $post instanceof WP_Post ) {
232 $user_text .= 'Post title: ' . wp_strip_all_tags( $post->post_title ) . "\n";
233 }
234
235 $post_analysis = desktop_mode_ai_get_meta( 'post', $post_id );
236 if ( $post_analysis && ! empty( $post_analysis['ai_summary'] ) ) {
237 $user_text .= 'Post summary: ' . $post_analysis['ai_summary'] . "\n";
238 }
239 }
240
241 $user_text .= "\n\nClassification rules:\n";
242 $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";
243 $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";
244 $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";
245 $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.";
246
247 /**
248 * Filters the user message sent to OpenAI for comment analysis.
249 *
250 * @since 0.14.0
251 *
252 * @param string $user_text The composed user message.
253 * @param WP_Comment $comment The comment being analyzed.
254 */
255 $user_text = (string) apply_filters( 'desktop_mode_ai_comment_prompt', $user_text, $comment );
256
257 return array(
258 array(
259 'role' => 'system',
260 '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.',
261 ),
262 array(
263 'role' => 'user',
264 'content' => $user_text,
265 ),
266 );
267 }
268
269 // ---------------------------------------------------------------------------
270 // Meta read / write
271 // ---------------------------------------------------------------------------
272
273 /**
274 * Saves an AI analysis result as meta for the given entity.
275 *
276 * @since 0.14.0
277 *
278 * @param string $entity_type 'post' | 'term' | 'comment'.
279 * @param int $entity_id
280 * @param array $analysis The structured output array from OpenAI.
281 * @return bool
282 */
283 function desktop_mode_ai_save_meta( $entity_type, $entity_id, array $analysis ) {
284 $entity_id = (int) $entity_id;
285 if ( $entity_id <= 0 ) {
286 return false;
287 }
288
289 // Stamp when the analysis was performed so consumers can detect staleness.
290 $analysis['analyzed_at'] = time();
291
292 switch ( $entity_type ) {
293 case 'post':
294 return false !== update_post_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, $analysis );
295
296 case 'term':
297 return false !== update_term_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, $analysis );
298
299 case 'comment':
300 return false !== update_comment_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, $analysis );
301 }
302
303 return false;
304 }
305
306 /**
307 * Retrieves a previously saved AI analysis, or null if none exists.
308 *
309 * @since 0.14.0
310 *
311 * @param string $entity_type 'post' | 'term' | 'comment'.
312 * @param int $entity_id
313 * @return array|null
314 */
315 function desktop_mode_ai_get_meta( $entity_type, $entity_id ) {
316 $entity_id = (int) $entity_id;
317 if ( $entity_id <= 0 ) {
318 return null;
319 }
320
321 $raw = null;
322 switch ( $entity_type ) {
323 case 'post':
324 $raw = get_post_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, true );
325 break;
326 case 'term':
327 $raw = get_term_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, true );
328 break;
329 case 'comment':
330 $raw = get_comment_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, true );
331 break;
332 }
333
334 return is_array( $raw ) && ! empty( $raw ) ? $raw : null;
335 }
336