PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.1
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.1
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Utils / AIHelper.php

AIHelper.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.5.1, at includes/Utils/AIHelper.php

478 lines 16.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Utils;
4
5 use WPDeveloper\BetterDocs\Core\Settings;
6
7 class AIHelper {
8
9 /**
10 * Settings instance
11 *
12 * @var Settings
13 */
14 private $settings;
15
16 public function __construct( Settings $settings ) {
17 $this->settings = $settings;
18 }
19
20 /**
21 * Get OpenAI API key from settings
22 *
23 * @return string
24 */
25 public function get_api_key() {
26 return $this->settings->get( 'ai_autowrite_api_key', '' );
27 }
28
29 /**
30 * Check if OpenAI API key is configured
31 *
32 * @return bool
33 */
34 public function has_api_key() {
35 $api_key = $this->get_api_key();
36 return ! empty( $api_key );
37 }
38
39 /**
40 * Validate OpenAI API key
41 *
42 * @param string $api_key Optional API key to validate, uses stored key if not provided
43 * @return array Array with 'valid' boolean and 'message' string
44 */
45 public function validate_api_key( $api_key = '' ) {
46 if ( empty( $api_key ) ) {
47 $api_key = $this->get_api_key();
48 }
49
50 if ( empty( $api_key ) ) {
51 return array(
52 'valid' => false,
53 'message' => 'Please Insert your <a href="/admin.php?page=betterdocs-settings#betterdocs-ai">OpenAI API Key</a> to use AI features.'
54 );
55 }
56
57 $ch = curl_init( 'https://api.openai.com/v1/models' ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init
58 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
59 curl_setopt( //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
60 $ch,
61 CURLOPT_HTTPHEADER,
62 array(
63 'Content-Type: application/json',
64 'Authorization: Bearer ' . $api_key
65 )
66 );
67
68 $response = curl_exec( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec
69 $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo
70 curl_close( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
71
72 if ( 200 == $httpCode ) {
73 return array(
74 'valid' => true,
75 'message' => 'Valid API Key'
76 );
77 } else {
78 $responseData = json_decode( $response, true );
79 $messageData = $responseData[ 'error' ] ?? '';
80 return array(
81 'valid' => false,
82 'message' => $messageData[ 'message' ] ?? 'Invalid API Key'
83 );
84 }
85 }
86
87 /**
88 * Make a request to OpenAI API
89 *
90 * @param array $messages Array of messages for the chat completion
91 * @param array $options Optional parameters (model, max_tokens, temperature, etc.)
92 * @return string|\WP_Error API response content or error
93 */
94 public function make_openai_request( $messages, $options = array() ) {
95 $api_key = $this->get_api_key();
96 $max_tokens = $this->settings->get( 'article_summary_max_token', 1500 );
97 $model = $this->settings->get( 'article_summary_model', 'gpt-4o-mini' );
98
99 if ( empty( $api_key ) ) {
100 return new \WP_Error( 'no_api_key', 'OpenAI API key is not configured.' );
101 }
102
103 // Default options
104 $defaults = array(
105 'model' => $model,
106 'max_tokens' => $max_tokens,
107 'temperature' => 0.7,
108 'timeout' => 50
109 );
110
111 $options = wp_parse_args( $options, $defaults );
112
113 $api_endpoint = 'https://api.openai.com/v1/chat/completions';
114
115 $request_body = array(
116 'model' => $options[ 'model' ],
117 'messages' => $messages,
118 'max_tokens' => $options[ 'max_tokens' ],
119 'temperature' => $options[ 'temperature' ]
120 );
121
122 $request_options = array(
123 'headers' => array(
124 'Content-Type' => 'application/json',
125 'Authorization' => 'Bearer ' . $api_key
126 ),
127 'body' => json_encode( $request_body ), //phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
128 'timeout' => $options[ 'timeout' ]
129 );
130
131 $response = wp_remote_post( $api_endpoint, $request_options );
132
133 if ( is_wp_error( $response ) ) {
134 return new \WP_Error( 'api_error', 'Failed to connect to OpenAI API: ' . $response->get_error_message() );
135 }
136
137 $body = wp_remote_retrieve_body( $response );
138 $data = json_decode( $body, true );
139
140 if ( ! empty( $data[ 'error' ] ) ) {
141 return new \WP_Error( 'openai_error', $data[ 'error' ][ 'message' ] );
142 }
143
144 if ( empty( $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ] ) ) {
145 return new \WP_Error( 'no_content', 'No content received from OpenAI.' );
146 }
147
148 return $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ];
149 }
150
151 /**
152 * Analyze article quality using OpenAI
153 *
154 * @param string $content Article content to analyze
155 * @param string $title Article title (optional)
156 * @return array|\WP_Error Analysis result with score and feedback
157 */
158 public function analyze_article_quality( $content, $title = '' ) {
159 if ( empty( $content ) ) {
160 return new \WP_Error( 'empty_content', 'Article content cannot be empty.' );
161 }
162
163 // Create analysis prompt
164 $prompt = $this->build_quality_analysis_prompt( $content, $title );
165
166 $messages = array(
167 array(
168 'role' => 'system',
169 'content' => 'You are an expert content analyst specializing in documentation quality assessment. Provide detailed, actionable feedback to help improve article quality.'
170 ),
171 array(
172 'role' => 'user',
173 'content' => $prompt
174 )
175 );
176
177 $options = array(
178 'max_tokens' => 2000,
179 'temperature' => 0.3 // Lower temperature for more consistent analysis
180 );
181
182 $response = $this->make_openai_request( $messages, $options );
183
184 if ( is_wp_error( $response ) ) {
185 return $response;
186 }
187
188 // Parse the AI response into structured data
189 return $this->parse_quality_analysis_response( $response );
190 }
191
192 /**
193 * Build the prompt for article quality analysis
194 *
195 * @param string $content Article content
196 * @param string $title Article title
197 * @return string Formatted prompt
198 */
199 private function build_quality_analysis_prompt( $content, $title = '' ) {
200 $title_section = ! empty( $title ) ? "Title: {$title}\n\n" : '';
201
202 $prompt = "Please analyze the following documentation article for quality and provide a comprehensive assessment:\n\n";
203 $prompt .= $title_section;
204 $prompt .= "Content:\n{$content}\n\n";
205 $prompt .= "Please evaluate the article based on these criteria and provide your response in the following JSON format:\n\n";
206 $prompt .= "{\n";
207 $prompt .= ' "overall_score": 91,';
208 $prompt .= ' "scores": {';
209 $prompt .= ' "clarity": 90,';
210 $prompt .= ' "completeness": 92,';
211 $prompt .= ' "relevance": 95,';
212 $prompt .= ' "structure": 87,';
213 $prompt .= ' "readability": 88';
214 $prompt .= ' },';
215 $prompt .= ' "feedback": {';
216 $prompt .= ' "strengths": ["Clear headings", "Good use of examples"],';
217 $prompt .= ' "improvements": ["Add more detailed explanations in section 2", "Include troubleshooting steps"],';
218 $prompt .= ' "suggestions": ["Consider adding screenshots", "Break down complex paragraphs"]';
219 $prompt .= ' },';
220 $prompt .= ' "priority": "medium"';
221 $prompt .= "}\n\n";
222 $prompt .= "Scoring criteria (0-100):\n";
223 $prompt .= "- Clarity: How clear and understandable is the content?\n";
224 $prompt .= "- Completeness: Does it cover the topic thoroughly?\n";
225 $prompt .= "- Relevance: Is the content relevant to the stated purpose?\n";
226 $prompt .= "- Structure: Is the content well-organized with proper headings?\n";
227 $prompt .= "- Readability: Is it easy to read and follow?\n\n";
228 $prompt .= "Priority levels: low (80+), medium (60-79), high (below 60)\n";
229 $prompt .= "Provide specific, actionable feedback that content creators can implement.";
230
231 return $prompt;
232 }
233
234 /**
235 * Parse AI response into structured quality analysis data
236 *
237 * @param string $response Raw AI response
238 * @return array|\WP_Error Parsed analysis data
239 */
240 private function parse_quality_analysis_response( $response ) {
241 // Try to extract JSON from the response
242 $json_start = strpos( $response, '{' );
243 $json_end = strrpos( $response, '}' );
244
245 if ( false === $json_start || false === $json_end ) {
246 return new \WP_Error( 'parse_error', 'Could not find valid JSON in AI response.' );
247 }
248
249 $json_string = substr( $response, $json_start, $json_end - $json_start + 1 );
250 $data = json_decode( $json_string, true );
251
252 if ( json_last_error() !== JSON_ERROR_NONE ) {
253 return new \WP_Error( 'json_error', 'Invalid JSON in AI response: ' . json_last_error_msg() );
254 }
255
256 // Validate required fields
257 $required_fields = array( 'overall_score', 'scores', 'feedback' );
258 foreach ( $required_fields as $field ) {
259 if ( ! isset( $data[ $field ] ) ) {
260 return new \WP_Error( 'missing_field', "Missing required field: {$field}" );
261 }
262 }
263
264 // Ensure scores are within valid range
265 $data[ 'overall_score' ] = max( 0, min( 100, intval( $data[ 'overall_score' ] ) ) );
266
267 if ( isset( $data[ 'scores' ] ) && is_array( $data[ 'scores' ] ) ) {
268 foreach ( $data[ 'scores' ] as $key => $score ) {
269 $data[ 'scores' ][ $key ] = max( 0, min( 100, intval( $score ) ) );
270 }
271 }
272
273 // Set default priority if not provided
274 if ( ! isset( $data[ 'priority' ] ) ) {
275 $overall_score = $data[ 'overall_score' ];
276 if ( $overall_score >= 80 ) {
277 $data[ 'priority' ] = 'low';
278 } elseif ( $overall_score >= 60 ) {
279 $data[ 'priority' ] = 'medium';
280 } else {
281 $data[ 'priority' ] = 'high';
282 }
283 }
284
285 return $data;
286 }
287
288 /**
289 * Save article quality score as post meta
290 *
291 * @param int $post_id Post ID
292 * @param array $quality_data Quality analysis data
293 * @return bool Success status
294 */
295 public function save_article_quality_score( $post_id, $quality_data ) {
296 if ( empty( $post_id ) || ! is_array( $quality_data ) ) {
297 return false;
298 }
299
300 // Save the complete analysis data
301 $saved = update_post_meta( $post_id, '_betterdocs_article_quality_analysis', $quality_data );
302
303 // Save just the overall score for easy querying
304 update_post_meta( $post_id, '_betterdocs_article_quality_score', $quality_data[ 'overall_score' ] );
305
306 // Save analysis timestamp
307 update_post_meta( $post_id, '_betterdocs_article_quality_analyzed_at', current_time( 'mysql' ) );
308
309 return false !== $saved;
310 }
311
312 /**
313 * Get article quality score from post meta
314 *
315 * @param int $post_id Post ID
316 * @return array|false Quality analysis data or false if not found
317 */
318 public function get_article_quality_score( $post_id ) {
319 if ( empty( $post_id ) ) {
320 return false;
321 }
322
323 $quality_data = get_post_meta( $post_id, '_betterdocs_article_quality_analysis', true );
324
325 if ( empty( $quality_data ) ) {
326 return false;
327 }
328
329 // Add timestamp if available
330 $analyzed_at = get_post_meta( $post_id, '_betterdocs_article_quality_analyzed_at', true );
331 if ( $analyzed_at ) {
332 $quality_data[ 'analyzed_at' ] = $analyzed_at;
333 }
334
335 return $quality_data;
336 }
337
338 /**
339 * Check if article needs re-analysis based on last modified date
340 *
341 * @param int $post_id Post ID
342 * @return bool True if re-analysis is needed
343 */
344 public function needs_reanalysis( $post_id ) {
345 $analyzed_at = get_post_meta( $post_id, '_betterdocs_article_quality_analyzed_at', true );
346
347 if ( empty( $analyzed_at ) ) {
348 return true; // Never analyzed
349 }
350
351 $post = get_post( $post_id );
352 if ( ! $post ) {
353 return false;
354 }
355
356 // Check if post was modified after last analysis
357 $post_modified = strtotime( $post->post_modified );
358 $analyzed_timestamp = strtotime( $analyzed_at );
359
360 return $post_modified > $analyzed_timestamp;
361 }
362
363 /**
364 * Create a system message for OpenAI
365 *
366 * @param string $content System message content
367 * @return array Message array
368 */
369 public function create_system_message( $content ) {
370 return array(
371 'role' => 'system',
372 'content' => $content
373 );
374 }
375
376 /**
377 * Create a user message for OpenAI
378 *
379 * @param string $content User message content
380 * @return array Message array
381 */
382 public function create_user_message( $content ) {
383 return array(
384 'role' => 'user',
385 'content' => $content
386 );
387 }
388
389 /**
390 * Create messages array for article summarization
391 *
392 * @param string $title Article title
393 * @param string $content Article content
394 * @return array Messages array
395 */
396 public function create_summary_messages( $title, $content ) {
397 $system_message = $this->create_system_message(
398 'You are a helpful assistant that creates concise, informative summaries of documentation articles. Always format your response in clean HTML with paragraph tags. Do not use markdown formatting, code blocks, or backticks. Return only the HTML content without any wrapper formatting.'
399 );
400
401 $user_prompt = "Please provide a concise summary of the following article titled '{$title}'. The summary should be 2-3 paragraphs long, highlighting the main points and key takeaways. Format the response in HTML with proper paragraph tags. Do not wrap the response in markdown code blocks or use any markdown formatting.\n\nArticle content:\n{$content}";
402
403 $user_message = $this->create_user_message( $user_prompt );
404
405 return array( $system_message, $user_message );
406 }
407
408 /**
409 * Create messages array for content generation
410 *
411 * @param string $prompt User prompt
412 * @param string $keywords Optional keywords
413 * @return array Messages array
414 */
415 public function create_content_messages( $prompt, $keywords = '' ) {
416 $system_message = $this->create_system_message(
417 'You are a helpful assistant who writes documentation for users.'
418 );
419
420 $user_message = $this->create_user_message( $prompt );
421
422 return array( $system_message, $user_message );
423 }
424
425 /**
426 * Sanitize and prepare content for AI processing
427 *
428 * @param string $content Raw content
429 * @param int $max_length Maximum length to keep
430 * @return string Sanitized content
431 */
432 public function prepare_content_for_ai( $content, $max_length = 4000 ) {
433 // Strip HTML tags and decode entities
434 $content = wp_strip_all_tags( $content );
435 $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
436
437 // Remove extra whitespace
438 $content = preg_replace( '/\s+/', ' ', $content );
439 $content = trim( $content );
440
441 // Limit length
442 if ( strlen( $content ) > $max_length ) {
443 $content = substr( $content, 0, $max_length );
444 // Try to cut at a word boundary
445 $last_space = strrpos( $content, ' ' );
446 if ( false !== $last_space && $last_space > $max_length * 0.8 ) {
447 $content = substr( $content, 0, $last_space );
448 }
449 $content .= '...';
450 }
451
452 return $content;
453 }
454
455 /**
456 * Check if AI features are enabled
457 *
458 * @return bool
459 */
460 public function is_ai_enabled() {
461 return $this->settings->get( 'enable_write_with_ai', true ) && $this->has_api_key();
462 }
463
464 /**
465 * Get AI usage statistics (placeholder for future implementation)
466 *
467 * @return array Usage statistics
468 */
469 public function get_usage_stats() {
470 // This could be implemented to track API usage, costs, etc.
471 return array(
472 'requests_today' => 0,
473 'tokens_used' => 0,
474 'cost_estimate' => 0
475 );
476 }
477 }
478