| 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">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 |
|