PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.1
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / ai / class-openai-client.php

class-openai-client.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.1, at includes/ai/class-openai-client.php

854 lines 31.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenAI API Client
4 *
5 * Handles communication with OpenAI API
6 *
7 * @package ThinkRank\AI
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\AI;
14
15 // Prevent direct access
16 if (!defined('ABSPATH')) {
17 exit;
18 }
19
20 /**
21 * OpenAI Client Class
22 *
23 * Single Responsibility: Handle OpenAI API communication
24 *
25 * @since 1.0.0
26 */
27 class OpenAI_Client {
28
29 /**
30 * OpenAI API base URL
31 */
32 private const API_BASE_URL = 'https://api.openai.com/v1';
33
34 /**
35 * API key
36 *
37 * @var string
38 */
39 private string $api_key;
40
41 /**
42 * Default model
43 *
44 * @var string
45 */
46 private string $model;
47
48 /**
49 * Request timeout in seconds
50 *
51 * @var int
52 */
53 private int $timeout;
54
55 /**
56 * Prompt Builder instance
57 *
58 * @since 1.0.0
59 * @var Prompt_Builder|null
60 */
61 private ?Prompt_Builder $prompt_builder = null;
62
63 /**
64 * Constructor
65 *
66 * @param string $api_key OpenAI API key
67 * @param string $model Default model to use
68 * @param int $timeout Request timeout
69 */
70 public function __construct(string $api_key, string $model = 'gpt-5-nano', int $timeout = 30) {
71 $this->api_key = $api_key;
72 $this->model = $model;
73 $this->timeout = $timeout;
74 }
75
76 /**
77 * Get Prompt Builder instance
78 *
79 * @since 1.0.0
80 *
81 * @return Prompt_Builder Prompt Builder instance
82 */
83 private function get_prompt_builder(): Prompt_Builder {
84 if (!$this->prompt_builder) {
85 // Ensure Prompt Builder is loaded
86 if (!class_exists('ThinkRank\\AI\\Prompt_Builder')) {
87 require_once THINKRANK_PLUGIN_DIR . 'includes/ai/class-prompt-builder.php';
88 }
89 $this->prompt_builder = new Prompt_Builder();
90 }
91 return $this->prompt_builder;
92 }
93
94 /**
95 * Generate completion using OpenAI
96 *
97 * @param string $prompt The prompt to send
98 * @param array $options Additional options
99 * @return array Response data
100 * @throws \Exception If API request fails
101 */
102 public function generate_completion(string $prompt, array $options = []): array {
103 $default_options = [
104 'model' => $this->model,
105 'max_tokens' => 1000,
106 'temperature' => 0.7,
107 'top_p' => 1,
108 'frequency_penalty' => 0,
109 'presence_penalty' => 0,
110 ];
111
112 $options = array_merge($default_options, $options);
113
114 $body = [
115 'model' => $options['model'],
116 'messages' => [
117 [
118 'role' => 'user',
119 'content' => $prompt,
120 ]
121 ],
122 ];
123
124 // Get safe token limit for this model
125 $safe_tokens = $this->get_safe_token_limit($options['model'], $options['max_tokens']);
126
127 // Add parameters based on model type
128 if ($this->is_reasoning_model($options['model'])) {
129 // Reasoning models (o1/o3) have fixed parameters and restricted support
130 // temperature, top_p, frequency_penalty, presence_penalty are not supported
131 $body['max_completion_tokens'] = $safe_tokens;
132 } else {
133 // Standard models support all parameters
134 $body['temperature'] = $options['temperature'];
135 $body['top_p'] = $options['top_p'];
136 $body['frequency_penalty'] = $options['frequency_penalty'];
137 $body['presence_penalty'] = $options['presence_penalty'];
138 $body['max_tokens'] = $safe_tokens;
139 }
140
141 return $this->make_request('chat/completions', $body);
142 }
143
144 /**
145 * Generate SEO metadata
146 *
147 * @param string $content Content to analyze
148 * @param array $options Generation options
149 * @return array Generated metadata
150 * @throws \Exception If generation fails
151 */
152 public function generate_seo_metadata(string $content, array $options = []): array {
153 $target_keyword = $options['target_keyword'] ?? '';
154 $content_type = $options['content_type'] ?? 'blog_post';
155 $tone = $options['tone'] ?? 'professional';
156
157 $prompt_builder = $this->get_prompt_builder();
158 $prompt = $prompt_builder->build_seo_prompt($content, $target_keyword, $content_type, $tone, 'openai');
159
160 $response = $this->generate_completion($prompt, [
161 'max_tokens' => $this->get_recommended_tokens('seo_metadata'),
162 'temperature' => 0.3, // Lower temperature for more consistent SEO output
163 ]);
164
165 return $this->parse_seo_response($response);
166 }
167
168 /**
169 * Analyze content for SEO optimization
170 *
171 * @param string $content Content to analyze
172 * @param array $metadata Existing metadata
173 * @return array Analysis results
174 * @throws \Exception If analysis fails
175 */
176 public function analyze_content(string $content, array $metadata = []): array {
177 $prompt_builder = $this->get_prompt_builder();
178 $prompt = $prompt_builder->build_analysis_prompt($content, $metadata, 'openai');
179
180 $response = $this->generate_completion($prompt, [
181 'max_tokens' => $this->get_recommended_tokens('analysis'),
182 'temperature' => 0.3, // Lower temperature for more consistent analysis
183 ]);
184
185 return $this->parse_analysis_response($response);
186 }
187
188 /**
189 * Check if model uses max_completion_tokens parameter
190 *
191 * @param string $model Model name
192 * @return bool True if model uses max_completion_tokens
193 */
194 private function uses_max_completion_tokens(string $model): bool {
195 return $this->is_reasoning_model($model);
196 }
197
198 /**
199 * Check if model is a reasoning model (o1/o3 series)
200 *
201 * @param string $model Model name
202 * @return bool True if model is a reasoning model
203 */
204 private function is_reasoning_model(string $model): bool {
205 // Models that require max_completion_tokens and restrict parameters (no temperature/top_p)
206 // Includes OpenAI o1/o3 series and GPT-5 family
207 $reasoning_models = [
208 'o1-preview',
209 'o1-mini',
210 'o3-mini',
211 'o3-2024-12-17',
212 'gpt-5',
213 'gpt-5-mini',
214 'gpt-5-nano',
215 ];
216
217 // Check for exact matches or model prefixes
218 foreach ($reasoning_models as $reasoning_model) {
219 if ($model === $reasoning_model || strpos($model, $reasoning_model) === 0) {
220 return true;
221 }
222 }
223
224 return false;
225 }
226
227 /**
228 * Get maximum completion tokens for a model
229 *
230 * @param string $model Model name
231 * @return int Maximum completion tokens
232 */
233 private function get_max_completion_tokens(string $model): int {
234 // Model-specific token limits (completion tokens, not context)
235 $token_limits = [
236 // GPT-5 series (optimized for reasoning + content tokens based on usage data)
237 'gpt-5' => 20480, // 20K tokens for full GPT-5
238 'gpt-5-mini' => 15360, // 15K tokens for mini variant (increased)
239 'gpt-5-nano' => 12288, // 12K tokens for nano (increased from 10K for better buffer)
240
241 // GPT-4o series
242 'gpt-4o' => 4096,
243 'gpt-4o-2024-08-06' => 4096,
244 'gpt-4o-2024-05-13' => 4096,
245 'gpt-4o-mini' => 16384,
246 'gpt-4o-mini-2024-07-18' => 16384,
247
248 // o1/o3 reasoning models (higher limits)
249 'o1-preview' => 32768,
250 'o1-mini' => 65536,
251 'o3-mini' => 65536,
252 'o3-2024-12-17' => 65536,
253 ];
254
255 // Check for exact match first
256 if (isset($token_limits[$model])) {
257 return $token_limits[$model];
258 }
259
260 // Check for partial matches (for versioned models)
261 foreach ($token_limits as $known_model => $limit) {
262 if (strpos($model, $known_model) === 0) {
263 return $limit;
264 }
265 }
266
267 // Default fallback for unknown models
268 return 4096;
269 }
270
271 /**
272 * Get safe token limit for a request
273 *
274 * @param string $model Model name
275 * @param int $requested_tokens Requested token count
276 * @return int Safe token count (capped at model limit)
277 */
278 public function get_safe_token_limit(string $model, int $requested_tokens): int {
279 $max_tokens = $this->get_max_completion_tokens($model);
280 return min($requested_tokens, $max_tokens);
281 }
282
283 /**
284 * Get recommended token limit for specific use cases
285 *
286 * @param string $use_case Use case (e.g., 'content_brief', 'seo_metadata', 'analysis')
287 * @return int Recommended token limit
288 */
289 public function get_recommended_tokens(string $use_case): int {
290 $max_tokens = $this->get_max_completion_tokens($this->model);
291
292 // For reasoning models (GPT-5, o1, o3), we need much higher token limits
293 // because they use reasoning tokens + content tokens
294 if ($this->is_reasoning_model($this->model)) {
295 $reasoning_recommendations = [
296 'content_brief' => 0.95, // 95% of max tokens (reasoning + content)
297 'seo_metadata' => 0.8, // 80% for reasoning models (safe buffer)
298 'analysis' => 0.85, // 85% for reasoning models (safe buffer)
299 'llms_txt' => 0.8, // 80% for reasoning models (safe buffer)
300 'optimization' => 0.9, // 90% for reasoning models (based on 5.9K usage + 50% buffer)
301 ];
302 $percentage = $reasoning_recommendations[$use_case] ?? 0.3;
303 } else {
304 // Standard models - original percentages
305 $standard_recommendations = [
306 'content_brief' => 0.9, // 90% of max tokens for comprehensive briefs
307 'seo_metadata' => 0.15, // 15% of max tokens for metadata
308 'analysis' => 0.25, // 25% of max tokens for analysis
309 'llms_txt' => 0.5, // 50% of max tokens for llms.txt
310 'optimization' => 0.15, // 15% of max tokens for optimization
311 ];
312 $percentage = $standard_recommendations[$use_case] ?? 0.15;
313 }
314
315 return (int) ($max_tokens * $percentage);
316 }
317
318 /**
319 * Build request body for chat completions with model-specific parameters
320 *
321 * @param string $user_prompt User prompt
322 * @param string|null $system_prompt System prompt (ignored for reasoning models)
323 * @param int $max_tokens Maximum tokens
324 * @param float $temperature Temperature (ignored for reasoning models)
325 * @return array Request body
326 */
327 private function build_chat_request(string $user_prompt, ?string $system_prompt = null, int $max_tokens = 600, float $temperature = 0.4): array {
328 $body = [
329 'model' => $this->model,
330 ];
331
332 // Get safe token limit for this model
333 $safe_tokens = $this->get_safe_token_limit($this->model, $max_tokens);
334
335 // Build messages based on model type
336 if ($this->is_reasoning_model($this->model)) {
337 // Reasoning/GPT‑5 family: use max_completion_tokens and omit temperature
338 $content = $user_prompt;
339 // Some GPT‑5 responses may expect array content parts; we'll send string
340 $body['messages'] = [
341 [
342 'role' => 'user',
343 'content' => $content
344 ]
345 ];
346 $body['max_completion_tokens'] = $safe_tokens;
347 } else {
348 // Standard models support system messages and temperature
349 $messages = [];
350 if ($system_prompt) {
351 $messages[] = [
352 'role' => 'system',
353 'content' => $system_prompt
354 ];
355 }
356 $messages[] = [
357 'role' => 'user',
358 'content' => $user_prompt
359 ];
360
361 $body['messages'] = $messages;
362 $body['temperature'] = $temperature;
363 $body['max_tokens'] = $safe_tokens;
364 }
365
366 return $body;
367 }
368
369 /**
370 * Get current model
371 *
372 * @return string Current model name
373 */
374 public function get_model(): string {
375 return $this->model;
376 }
377
378 /**
379 * Test API connection
380 *
381 * @return bool True if connection successful
382 */
383 public function test_connection(): bool {
384 try {
385 $response = $this->make_request('models');
386 return isset($response['data']) && is_array($response['data']);
387 } catch (\Exception $e) {
388 return false;
389 }
390 }
391
392 /**
393 * Make API request to OpenAI
394 *
395 * @param string $endpoint API endpoint
396 * @param array $body Request body
397 * @return array Response data
398 * @throws \Exception If request fails
399 */
400 private function make_request(string $endpoint, array $body = []): array {
401 $url = self::API_BASE_URL . '/' . ltrim($endpoint, '/');
402
403 $args = [
404 'timeout' => $this->timeout,
405 'headers' => [
406 'Authorization' => 'Bearer ' . $this->api_key,
407 'Content-Type' => 'application/json',
408 'User-Agent' => 'ThinkRank/' . THINKRANK_VERSION,
409 ],
410 ];
411
412 if (!empty($body)) {
413 $args['method'] = 'POST';
414 $args['body'] = wp_json_encode($body);
415 }
416
417 $response = wp_remote_request($url, $args);
418
419 if (is_wp_error($response)) {
420 throw new \Exception('API request failed: ' . esc_html($response->get_error_message()));
421 }
422
423 $status_code = wp_remote_retrieve_response_code($response);
424 $response_body = wp_remote_retrieve_body($response);
425
426 if ($status_code >= 400) {
427 $error_data = json_decode($response_body, true);
428 $error_message = $error_data['error']['message'] ?? 'Unknown API error';
429 throw new \Exception(sprintf('OpenAI API error (%d): %s', (int) $status_code, esc_html($error_message)));
430 }
431
432 $data = json_decode($response_body, true);
433
434 if (json_last_error() !== JSON_ERROR_NONE) {
435 throw new \Exception('Invalid JSON response from OpenAI API');
436 }
437
438 return $data;
439 }
440
441
442
443 /**
444 * Parse SEO response from OpenAI
445 *
446 * @param array $response OpenAI response
447 * @return array Parsed metadata
448 * @throws \Exception If parsing fails
449 */
450 private function parse_seo_response(array $response): array {
451 if (!isset($response['choices'][0]['message']['content'])) {
452 throw new \Exception('Invalid response format from OpenAI');
453 }
454
455 $content = $response['choices'][0]['message']['content'];
456 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
457
458
459
460 // Try to extract JSON from the response
461 $json_start = strpos($content, '{');
462 $json_end = strrpos($content, '}');
463
464 if (false === $json_start || false === $json_end) {
465 throw new \Exception('No valid JSON found in OpenAI response');
466 }
467
468 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
469 $metadata = json_decode($json_content, true);
470
471 if (json_last_error() !== JSON_ERROR_NONE) {
472 throw new \Exception('Failed to parse JSON from OpenAI response');
473 }
474
475 // Validate required fields
476 $required_fields = ['title', 'description', 'focus_keyword'];
477 foreach ($required_fields as $field) {
478 if (!isset($metadata[$field])) {
479 throw new \Exception(sprintf('Missing required field: %s', esc_html($field)));
480 }
481 }
482
483 return [
484 'title' => sanitize_text_field($metadata['title']),
485 'description' => sanitize_text_field($metadata['description']),
486 'focus_keyword' => sanitize_text_field($metadata['focus_keyword']),
487 'suggestions' => array_map('sanitize_text_field', $metadata['suggestions'] ?? []),
488 'generated_at' => current_time('mysql'),
489 'tokens_used' => $response['usage']['total_tokens'] ?? 0,
490 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
491 ];
492 }
493
494 /**
495 * Parse analysis response from OpenAI
496 *
497 * @param array $response OpenAI API response
498 * @return array Parsed analysis data
499 * @throws \Exception If parsing fails
500 */
501 private function parse_analysis_response(array $response): array {
502 if (!isset($response['choices'][0]['message']['content'])) {
503 throw new \Exception('Invalid response format from OpenAI');
504 }
505
506 $content = trim($response['choices'][0]['message']['content']);
507 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
508
509 // Extract JSON from response
510 $json_start = strpos($content, '{');
511 $json_end = strrpos($content, '}');
512
513 if (false === $json_start || false === $json_end) {
514 throw new \Exception('No valid JSON found in response');
515 }
516
517 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
518 $analysis = json_decode($json_content, true);
519
520 if (json_last_error() !== JSON_ERROR_NONE) {
521 throw new \Exception('Failed to parse JSON response: ' . esc_html(json_last_error_msg()));
522 }
523
524 // Validate and sanitize response
525 return [
526 'seo_score' => min(100, max(0, (int) ($analysis['seo_score'] ?? 0))),
527 'content_analysis' => [
528 'word_count' => (int) ($analysis['content_analysis']['word_count'] ?? 0),
529 'readability' => sanitize_text_field($analysis['content_analysis']['readability'] ?? 'unknown'),
530 'keyword_density' => sanitize_text_field($analysis['content_analysis']['keyword_density'] ?? 'unknown'),
531 'structure' => sanitize_text_field($analysis['content_analysis']['structure'] ?? 'unknown'),
532 ],
533 'suggestions' => array_map('sanitize_text_field', $analysis['suggestions'] ?? []),
534 'strengths' => array_map('sanitize_text_field', $analysis['strengths'] ?? []),
535 'weaknesses' => array_map('sanitize_text_field', $analysis['weaknesses'] ?? []),
536 'analyzed_at' => current_time('mysql'),
537 'tokens_used' => $response['usage']['total_tokens'] ?? 0,
538 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
539 ];
540 }
541
542 /**
543 * Optimize site identity using OpenAI
544 *
545 * @since 1.0.0
546 *
547 * @param array $site_data Site data to optimize
548 * @param array $options Optimization options
549 * @return array Optimization results
550 * @throws \Exception If optimization fails
551 */
552 public function optimize_site_identity(array $site_data, array $options = []): array {
553 $business_type = $options['business_type'] ?? 'website';
554 $target_audience = $options['target_audience'] ?? 'general';
555 $tone = $options['tone'] ?? 'professional';
556
557 $prompt_builder = $this->get_prompt_builder();
558 $prompt = $prompt_builder->build_site_identity_prompt($site_data, $business_type, $target_audience, $tone, 'openai');
559
560 $body = $this->build_chat_request(
561 $prompt,
562 'You are an expert SEO consultant specializing in site identity optimization. Provide actionable, specific recommendations in JSON format.',
563 $this->get_recommended_tokens('optimization'),
564 0.4
565 );
566
567 $response = $this->make_request('chat/completions', $body);
568
569
570
571 return $this->parse_site_identity_response($response);
572 }
573
574
575
576 /**
577 * Parse site identity optimization response
578 *
579 * @param array $response OpenAI API response
580 * @return array Parsed optimization data
581 * @throws \Exception If parsing fails
582 */
583 private function parse_site_identity_response(array $response): array {
584 if (!isset($response['choices'][0]['message']['content'])) {
585 throw new \Exception('Invalid response format from OpenAI');
586 }
587
588 $content = trim($response['choices'][0]['message']['content']);
589 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
590
591 // Extract JSON from response
592 $json_start = strpos($content, '{');
593 $json_end = strrpos($content, '}');
594
595 if (false === $json_start || false === $json_end) {
596 throw new \Exception('No valid JSON found in response');
597 }
598
599 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
600 $optimization = json_decode($json_content, true);
601
602 if (json_last_error() !== JSON_ERROR_NONE) {
603 throw new \Exception('Failed to parse JSON response: ' . esc_html(json_last_error_msg()));
604 }
605
606 // Validate and sanitize response
607 return [
608 'optimized_data' => [
609 'site_name' => sanitize_text_field($optimization['optimized_data']['site_name'] ?? ''),
610 'site_description' => sanitize_text_field($optimization['optimized_data']['site_description'] ?? ''),
611 'tagline' => sanitize_text_field($optimization['optimized_data']['tagline'] ?? ''),
612 'default_meta_description' => sanitize_text_field($optimization['optimized_data']['default_meta_description'] ?? ''),
613 ],
614 'analysis' => sanitize_textarea_field($optimization['analysis'] ?? ''),
615 'suggestions' => array_map('sanitize_text_field', $optimization['suggestions'] ?? []),
616 'score' => min(100, max(0, (int) ($optimization['score'] ?? 0))),
617 'tokens_used' => $response['usage']['total_tokens'] ?? 0,
618 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
619 ];
620 }
621
622 /**
623 * Optimize homepage meta content using AI (copying Site Identity pattern exactly)
624 *
625 * @since 1.0.0
626 *
627 * @param array $content_data Meta content data to optimize
628 * @param array $options Optimization options
629 * @return array Optimization results
630 * @throws \Exception If optimization fails
631 */
632 public function optimize_homepage_meta(array $content_data, array $options = []): array {
633 $business_type = $options['business_type'] ?? 'website';
634 $target_audience = $options['target_audience'] ?? 'general';
635 $tone = $options['tone'] ?? 'professional';
636 $context = $options['context'] ?? [];
637
638 $prompt_builder = $this->get_prompt_builder();
639 $prompt = $prompt_builder->build_homepage_meta_prompt($content_data, $business_type, $target_audience, $tone, $context, 'openai');
640
641 $body = $this->build_chat_request(
642 $prompt,
643 'You are an expert SEO consultant specializing in homepage meta optimization. Provide actionable, specific recommendations in JSON format.',
644 $this->get_recommended_tokens('optimization'),
645 0.4
646 );
647
648 $response = $this->make_request('chat/completions', $body);
649
650 return $this->parse_homepage_meta_response($response);
651 }
652
653 /**
654 * Optimize homepage hero content using AI (copying Site Identity pattern exactly)
655 *
656 * @since 1.0.0
657 *
658 * @param array $hero_data Hero content data to optimize
659 * @param array $options Optimization options
660 * @return array Optimization results
661 * @throws \Exception If optimization fails
662 */
663 public function optimize_homepage_hero(array $hero_data, array $options = []): array {
664 $business_type = $options['business_type'] ?? 'website';
665 $target_audience = $options['target_audience'] ?? 'general';
666 $tone = $options['tone'] ?? 'professional';
667 $context = $options['context'] ?? [];
668
669 $prompt_builder = $this->get_prompt_builder();
670 $prompt = $prompt_builder->build_homepage_hero_prompt($hero_data, $business_type, $target_audience, $tone, $context, 'openai');
671
672 $body = $this->build_chat_request(
673 $prompt,
674 'You are an expert conversion optimization specialist specializing in homepage hero sections. Provide actionable, specific recommendations in JSON format.',
675 $this->get_recommended_tokens('optimization'),
676 0.4
677 );
678
679 $response = $this->make_request('chat/completions', $body);
680
681 return $this->parse_homepage_hero_response($response);
682 }
683
684 /**
685 * Optimize LLMs.txt content using OpenAI
686 *
687 * @since 1.0.0
688 *
689 * @param array $website_data Website data to optimize
690 * @param array $options Optimization options
691 * @return array Optimization results
692 * @throws \Exception If optimization fails
693 */
694 public function optimize_llms_txt(array $website_data, array $options = []): array {
695 // Use shared prompt builder for consistent prompts across all AI providers
696 $prompt_builder = $this->get_prompt_builder();
697 $prompt = $prompt_builder->build_llms_txt_prompt($website_data, $options, 'openai');
698
699 $body = $this->build_chat_request(
700 $prompt,
701 'You are an expert technical writer specializing in creating llms.txt files for AI assistants. Provide structured, comprehensive content in JSON format.',
702 $this->get_recommended_tokens('llms_txt'),
703 0.4
704 );
705
706 $response = $this->make_request('chat/completions', $body);
707
708 return $this->parse_llms_txt_response($response);
709 }
710
711
712
713 /**
714 * Parse LLMs.txt optimization response
715 *
716 * @param array $response OpenAI API response
717 * @return array Parsed optimization data
718 * @throws \Exception If parsing fails
719 */
720 private function parse_llms_txt_response(array $response): array {
721 if (!isset($response['choices'][0]['message']['content'])) {
722 throw new \Exception('Invalid response format from OpenAI');
723 }
724
725 $content = trim($response['choices'][0]['message']['content']);
726 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
727
728
729
730 // Extract JSON from response
731 $json_start = strpos($content, '{');
732 $json_end = strrpos($content, '}');
733
734 if (false === $json_start || false === $json_end) {
735 throw new \Exception('No valid JSON found in response');
736 }
737
738 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
739 $optimization = json_decode($json_content, true);
740
741 if (json_last_error() !== JSON_ERROR_NONE) {
742 throw new \Exception('Invalid JSON in response: ' . esc_html(json_last_error_msg()));
743 }
744
745 // Validate and sanitize response
746 return [
747 'optimized_data' => [
748 'site_name' => sanitize_text_field($optimization['optimized_data']['site_name'] ?? ''),
749 'project_overview' => sanitize_textarea_field($optimization['optimized_data']['project_overview'] ?? ''),
750 'key_features' => sanitize_textarea_field($optimization['optimized_data']['key_features'] ?? ''),
751 'architecture' => sanitize_textarea_field($optimization['optimized_data']['architecture'] ?? ''),
752 'development_guidelines' => sanitize_textarea_field($optimization['optimized_data']['development_guidelines'] ?? ''),
753 'ai_context' => sanitize_textarea_field($optimization['optimized_data']['ai_context'] ?? ''),
754 ],
755 'suggestions' => array_map('sanitize_text_field', $optimization['suggestions'] ?? []),
756 'tokens_used' => $response['usage']['total_tokens'] ?? 0,
757 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
758 ];
759 }
760
761
762
763
764
765 /**
766 * Parse homepage meta optimization response
767 *
768 * @param array $response OpenAI API response
769 * @return array Parsed optimization data
770 * @throws \Exception If parsing fails
771 */
772 private function parse_homepage_meta_response(array $response): array {
773 if (!isset($response['choices'][0]['message']['content'])) {
774 throw new \Exception('Invalid response format from OpenAI');
775 }
776
777 $content = trim($response['choices'][0]['message']['content']);
778 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
779
780 // Extract JSON from response
781 $json_start = strpos($content, '{');
782 $json_end = strrpos($content, '}');
783
784 if (false === $json_start || false === $json_end) {
785 throw new \Exception('No valid JSON found in response');
786 }
787
788 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
789 $optimization = json_decode($json_content, true);
790
791 if (json_last_error() !== JSON_ERROR_NONE) {
792 throw new \Exception('Failed to parse JSON response: ' . esc_html(json_last_error_msg()));
793 }
794
795 // Validate and sanitize response
796 return [
797 'optimized_data' => [
798 'title' => sanitize_text_field($optimization['optimized_data']['title'] ?? ''),
799 'meta_description' => sanitize_text_field($optimization['optimized_data']['meta_description'] ?? ''),
800 ],
801 'analysis' => sanitize_textarea_field($optimization['analysis'] ?? ''),
802 'suggestions' => array_map('sanitize_text_field', $optimization['suggestions'] ?? []),
803 'score' => min(100, max(0, (int) ($optimization['score'] ?? 0))),
804 'tokens_used' => $response['usage']['total_tokens'] ?? 0,
805 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
806 ];
807 }
808
809 /**
810 * Parse homepage hero optimization response
811 *
812 * @param array $response OpenAI API response
813 * @return array Parsed optimization data
814 * @throws \Exception If parsing fails
815 */
816 private function parse_homepage_hero_response(array $response): array {
817 if (!isset($response['choices'][0]['message']['content'])) {
818 throw new \Exception('Invalid response format from OpenAI');
819 }
820
821 $content = trim($response['choices'][0]['message']['content']);
822 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
823
824 // Extract JSON from response
825 $json_start = strpos($content, '{');
826 $json_end = strrpos($content, '}');
827
828 if (false === $json_start || false === $json_end) {
829 throw new \Exception('No valid JSON found in response');
830 }
831
832 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
833 $optimization = json_decode($json_content, true);
834
835 if (json_last_error() !== JSON_ERROR_NONE) {
836 throw new \Exception('Failed to parse JSON response: ' . esc_html(json_last_error_msg()));
837 }
838
839 // Validate and sanitize response
840 return [
841 'optimized_data' => [
842 'hero_title' => sanitize_text_field($optimization['optimized_data']['hero_title'] ?? ''),
843 'hero_subtitle' => sanitize_text_field($optimization['optimized_data']['hero_subtitle'] ?? ''),
844 'hero_cta_text' => sanitize_text_field($optimization['optimized_data']['hero_cta_text'] ?? '')
845 ],
846 'analysis' => sanitize_textarea_field($optimization['analysis'] ?? ''),
847 'suggestions' => array_map('sanitize_text_field', $optimization['suggestions'] ?? []),
848 'score' => min(100, max(0, (int) ($optimization['score'] ?? 0))),
849 'tokens_used' => $response['usage']['total_tokens'] ?? 0,
850 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
851 ];
852 }
853 }
854