PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.1.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.1.0
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-claude-client.php

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

652 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Claude API Client
4 *
5 * Handles communication with Anthropic Claude 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 * Claude Client Class
22 *
23 * Single Responsibility: Handle Claude API communication
24 *
25 * @since 1.0.0
26 */
27 class Claude_Client {
28
29 /**
30 * Claude API base URL
31 */
32 private const API_BASE_URL = 'https://api.anthropic.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 Claude 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 = 'claude-3-7-sonnet-latest', 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 Claude
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 ];
108
109 $options = array_merge($default_options, $options);
110
111 $body = [
112 'model' => $options['model'],
113 'max_tokens' => $options['max_tokens'],
114 'temperature' => $options['temperature'],
115 'messages' => [
116 [
117 'role' => 'user',
118 'content' => $prompt,
119 ]
120 ],
121 ];
122
123 return $this->make_request('messages', $body);
124 }
125
126 /**
127 * Generate SEO metadata
128 *
129 * @param string $content Content to analyze
130 * @param array $options Generation options
131 * @return array Generated metadata
132 * @throws \Exception If generation fails
133 */
134 public function generate_seo_metadata(string $content, array $options = []): array {
135 $target_keyword = $options['target_keyword'] ?? '';
136 $content_type = $options['content_type'] ?? 'blog_post';
137 $tone = $options['tone'] ?? 'professional';
138
139 $prompt_builder = $this->get_prompt_builder();
140 $prompt = $prompt_builder->build_seo_prompt($content, $target_keyword, $content_type, $tone, 'claude');
141
142 $response = $this->generate_completion($prompt, [
143 'max_tokens' => 500,
144 'temperature' => 0.3,
145 ]);
146
147 return $this->parse_seo_response($response);
148 }
149
150 /**
151 * Analyze content for SEO optimization
152 *
153 * @param string $content Content to analyze
154 * @param array $metadata Existing metadata
155 * @return array Analysis results
156 * @throws \Exception If analysis fails
157 */
158 public function analyze_content(string $content, array $metadata = []): array {
159 $prompt_builder = $this->get_prompt_builder();
160 $prompt = $prompt_builder->build_analysis_prompt($content, $metadata, 'claude');
161
162 $response = $this->generate_completion($prompt, [
163 'max_tokens' => 800,
164 'temperature' => 0.3,
165 ]);
166
167 return $this->parse_analysis_response($response);
168 }
169
170 /**
171 * Get current model
172 *
173 * @return string Current model name
174 */
175 public function get_model(): string {
176 return $this->model;
177 }
178
179 /**
180 * Test API connection
181 *
182 * @return bool True if connection successful
183 */
184 public function test_connection(): bool {
185 try {
186 // Claude doesn't have a models endpoint, so we'll test with a simple message
187 $response = $this->generate_completion('Hello', ['max_tokens' => 10]);
188 return isset($response['content']) && is_array($response['content']);
189 } catch (\Exception $e) {
190 return false;
191 }
192 }
193
194 /**
195 * Make API request to Claude
196 *
197 * @param string $endpoint API endpoint
198 * @param array $body Request body
199 * @return array Response data
200 * @throws \Exception If request fails
201 */
202 private function make_request(string $endpoint, array $body = []): array {
203 $url = self::API_BASE_URL . '/' . ltrim($endpoint, '/');
204
205 $args = [
206 'timeout' => $this->timeout,
207 'headers' => [
208 'x-api-key' => $this->api_key,
209 'Content-Type' => 'application/json',
210 'anthropic-version' => '2023-06-01',
211 'User-Agent' => 'ThinkRank/' . THINKRANK_VERSION,
212 ],
213 'method' => 'POST',
214 'body' => wp_json_encode($body),
215 ];
216
217 $response = wp_remote_request($url, $args);
218
219 if (is_wp_error($response)) {
220 throw new \Exception('API request failed: ' . esc_html($response->get_error_message()));
221 }
222
223 $status_code = wp_remote_retrieve_response_code($response);
224 $response_body = wp_remote_retrieve_body($response);
225
226 if ($status_code >= 400) {
227 $error_data = json_decode($response_body, true);
228 $error_message = $error_data['error']['message'] ?? 'Unknown API error';
229 throw new \Exception(sprintf('Claude API error (%d): %s', (int) $status_code, esc_html($error_message)));
230 }
231
232 $data = json_decode($response_body, true);
233
234 if (json_last_error() !== JSON_ERROR_NONE) {
235 throw new \Exception('Invalid JSON response from Claude API');
236 }
237
238 return $data;
239 }
240
241 /**
242 * Parse SEO response from Claude
243 *
244 * @param array $response Claude response
245 * @return array Parsed metadata
246 * @throws \Exception If parsing fails
247 */
248 private function parse_seo_response(array $response): array {
249 if (!isset($response['content'][0]['text'])) {
250 throw new \Exception('Invalid response format from Claude');
251 }
252
253 $content = $response['content'][0]['text'];
254 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
255
256 // Try to extract JSON from the response
257 $json_start = strpos($content, '{');
258 $json_end = strrpos($content, '}');
259
260 if (false === $json_start || false === $json_end) {
261 throw new \Exception('No valid JSON found in Claude response');
262 }
263
264 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
265 $metadata = json_decode($json_content, true);
266
267 if (json_last_error() !== JSON_ERROR_NONE) {
268 throw new \Exception('Failed to parse JSON from Claude response');
269 }
270
271 // Validate required fields
272 $required_fields = ['title', 'description', 'focus_keyword'];
273 foreach ($required_fields as $field) {
274 if (!isset($metadata[$field])) {
275 throw new \Exception(sprintf('Missing required field: %s', esc_html($field)));
276 }
277 }
278
279 return [
280 'title' => sanitize_text_field($metadata['title']),
281 'description' => sanitize_text_field($metadata['description']),
282 'focus_keyword' => sanitize_text_field($metadata['focus_keyword']),
283 'suggestions' => array_map('sanitize_text_field', $metadata['suggestions'] ?? []),
284 'generated_at' => current_time('mysql'),
285 'tokens_used' => $response['usage']['input_tokens'] + $response['usage']['output_tokens'] ?? 0,
286 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
287 ];
288 }
289
290 /**
291 * Parse analysis response from Claude
292 *
293 * @param array $response Claude API response
294 * @return array Parsed analysis data
295 * @throws \Exception If parsing fails
296 */
297 private function parse_analysis_response(array $response): array {
298 if (!isset($response['content'][0]['text'])) {
299 throw new \Exception('Invalid response format from Claude');
300 }
301
302 $content = trim($response['content'][0]['text']);
303 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
304
305 // Extract JSON from response
306 $json_start = strpos($content, '{');
307 $json_end = strrpos($content, '}');
308
309 if (false === $json_start || false === $json_end) {
310 throw new \Exception('No valid JSON found in response');
311 }
312
313 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
314 $analysis = json_decode($json_content, true);
315
316 if (json_last_error() !== JSON_ERROR_NONE) {
317 throw new \Exception('Failed to parse JSON response: ' . esc_html(json_last_error_msg()));
318 }
319
320 // Validate and sanitize response
321 return [
322 'seo_score' => min(100, max(0, (int) ($analysis['seo_score'] ?? 0))),
323 'content_analysis' => [
324 'word_count' => (int) ($analysis['content_analysis']['word_count'] ?? 0),
325 'readability' => sanitize_text_field($analysis['content_analysis']['readability'] ?? 'unknown'),
326 'keyword_density' => sanitize_text_field($analysis['content_analysis']['keyword_density'] ?? 'unknown'),
327 'structure' => sanitize_text_field($analysis['content_analysis']['structure'] ?? 'unknown'),
328 ],
329 'suggestions' => array_map('sanitize_text_field', $analysis['suggestions'] ?? []),
330 'strengths' => array_map('sanitize_text_field', $analysis['strengths'] ?? []),
331 'weaknesses' => array_map('sanitize_text_field', $analysis['weaknesses'] ?? []),
332 'analyzed_at' => current_time('mysql'),
333 'tokens_used' => ($response['usage']['input_tokens'] ?? 0) + ($response['usage']['output_tokens'] ?? 0),
334 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
335 ];
336 }
337
338 /**
339 * Optimize site identity using Claude
340 *
341 * @since 1.0.0
342 *
343 * @param array $site_data Site data to optimize
344 * @param array $options Optimization options
345 * @return array Optimization results
346 * @throws \Exception If optimization fails
347 */
348 public function optimize_site_identity(array $site_data, array $options = []): array {
349 $business_type = $options['business_type'] ?? 'website';
350 $target_audience = $options['target_audience'] ?? 'general';
351 $tone = $options['tone'] ?? 'professional';
352
353 $prompt_builder = $this->get_prompt_builder();
354 $prompt = $prompt_builder->build_site_identity_prompt($site_data, $business_type, $target_audience, $tone, 'claude');
355
356 $response = $this->make_request('messages', [
357 'model' => $this->model,
358 'max_tokens' => 600,
359 'temperature' => 0.4,
360 'messages' => [
361 [
362 'role' => 'user',
363 'content' => $prompt
364 ]
365 ]
366 ]);
367
368 return $this->parse_site_identity_response($response);
369 }
370
371 /**
372 * Parse site identity optimization response
373 *
374 * @param array $response Claude API response
375 * @return array Parsed optimization data
376 * @throws \Exception If parsing fails
377 */
378 private function parse_site_identity_response(array $response): array {
379 if (!isset($response['content'][0]['text'])) {
380 throw new \Exception('Invalid response format from Claude');
381 }
382
383 $content = trim($response['content'][0]['text']);
384 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
385
386 // Extract JSON from response
387 $json_start = strpos($content, '{');
388 $json_end = strrpos($content, '}');
389
390 if (false === $json_start || false === $json_end) {
391 throw new \Exception('No valid JSON found in response');
392 }
393
394 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
395 $optimization = json_decode($json_content, true);
396
397 if (json_last_error() !== JSON_ERROR_NONE) {
398 throw new \Exception('Failed to parse JSON response: ' . esc_html(json_last_error_msg()));
399 }
400
401 // Validate and sanitize response
402 return [
403 'optimized_data' => [
404 'site_name' => sanitize_text_field($optimization['optimized_data']['site_name'] ?? ''),
405 'site_description' => sanitize_text_field($optimization['optimized_data']['site_description'] ?? ''),
406 'tagline' => sanitize_text_field($optimization['optimized_data']['tagline'] ?? ''),
407 'default_meta_description' => sanitize_text_field($optimization['optimized_data']['default_meta_description'] ?? ''),
408 ],
409 'analysis' => sanitize_textarea_field($optimization['analysis'] ?? ''),
410 'suggestions' => array_map('sanitize_text_field', $optimization['suggestions'] ?? []),
411 'score' => min(100, max(0, (int) ($optimization['score'] ?? 0))),
412 'tokens_used' => ($response['usage']['input_tokens'] ?? 0) + ($response['usage']['output_tokens'] ?? 0),
413 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
414 ];
415 }
416
417 /**
418 * Optimize homepage meta content using AI (copying Site Identity pattern exactly)
419 *
420 * @since 1.0.0
421 *
422 * @param array $content_data Meta content data to optimize
423 * @param array $options Optimization options
424 * @return array Optimization results
425 * @throws \Exception If optimization fails
426 */
427 public function optimize_homepage_meta(array $content_data, array $options = []): array {
428 $business_type = $options['business_type'] ?? 'website';
429 $target_audience = $options['target_audience'] ?? 'general';
430 $tone = $options['tone'] ?? 'professional';
431 $context = $options['context'] ?? [];
432
433 $prompt_builder = $this->get_prompt_builder();
434 $prompt = $prompt_builder->build_homepage_meta_prompt($content_data, $business_type, $target_audience, $tone, $context, 'claude');
435
436 $response = $this->make_request('messages', [
437 'model' => $this->model,
438 'max_tokens' => 600,
439 'temperature' => 0.4,
440 'messages' => [
441 [
442 'role' => 'user',
443 'content' => $prompt
444 ]
445 ]
446 ]);
447
448 return $this->parse_homepage_meta_response($response);
449 }
450
451 /**
452 * Optimize homepage hero content using AI (copying Site Identity pattern exactly)
453 *
454 * @since 1.0.0
455 *
456 * @param array $hero_data Hero content data to optimize
457 * @param array $options Optimization options
458 * @return array Optimization results
459 * @throws \Exception If optimization fails
460 */
461 public function optimize_homepage_hero(array $hero_data, array $options = []): array {
462 $business_type = $options['business_type'] ?? 'website';
463 $target_audience = $options['target_audience'] ?? 'general';
464 $tone = $options['tone'] ?? 'professional';
465 $context = $options['context'] ?? [];
466
467 $prompt_builder = $this->get_prompt_builder();
468 $prompt = $prompt_builder->build_homepage_hero_prompt($hero_data, $business_type, $target_audience, $tone, $context, 'claude');
469
470 $response = $this->make_request('messages', [
471 'model' => $this->model,
472 'max_tokens' => 600,
473 'temperature' => 0.4,
474 'messages' => [
475 [
476 'role' => 'user',
477 'content' => $prompt
478 ]
479 ]
480 ]);
481
482 return $this->parse_homepage_hero_response($response);
483 }
484
485 /**
486 * Optimize LLMs.txt content using Claude
487 *
488 * @since 1.0.0
489 *
490 * @param array $website_data Website data to optimize
491 * @param array $options Optimization options
492 * @return array Optimization results
493 * @throws \Exception If optimization fails
494 */
495 public function optimize_llms_txt(array $website_data, array $options = []): array {
496 // Use shared prompt builder for consistent prompts across all AI providers
497 $prompt_builder = $this->get_prompt_builder();
498 $prompt = $prompt_builder->build_llms_txt_prompt($website_data, $options, 'claude');
499
500 $response = $this->make_request('messages', [
501 'model' => $this->model,
502 'max_tokens' => 2000, // Increased for consistency with other providers
503 'temperature' => 0.4,
504 'messages' => [
505 [
506 'role' => 'user',
507 'content' => $prompt
508 ]
509 ]
510 ]);
511
512 return $this->parse_llms_txt_response($response);
513 }
514
515
516
517 /**
518 * Parse LLMs.txt optimization response
519 *
520 * @param array $response Claude API response
521 * @return array Parsed optimization data
522 * @throws \Exception If parsing fails
523 */
524 private function parse_llms_txt_response(array $response): array {
525 if (!isset($response['content'][0]['text'])) {
526 throw new \Exception('Invalid response format from Claude');
527 }
528
529 $content = trim($response['content'][0]['text']);
530 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
531
532 // Extract JSON from response
533 $json_start = strpos($content, '{');
534 $json_end = strrpos($content, '}');
535
536 if (false === $json_start || false === $json_end) {
537 throw new \Exception('No valid JSON found in response');
538 }
539
540 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
541 $optimization = json_decode($json_content, true);
542
543 if (json_last_error() !== JSON_ERROR_NONE) {
544 throw new \Exception('Invalid JSON in response: ' . esc_html(json_last_error_msg()));
545 }
546
547 // Validate and sanitize response
548 return [
549 'optimized_data' => [
550 'site_name' => sanitize_text_field($optimization['optimized_data']['site_name'] ?? ''),
551 'project_overview' => sanitize_textarea_field($optimization['optimized_data']['project_overview'] ?? ''),
552 'key_features' => sanitize_textarea_field($optimization['optimized_data']['key_features'] ?? ''),
553 'architecture' => sanitize_textarea_field($optimization['optimized_data']['architecture'] ?? ''),
554 'development_guidelines' => sanitize_textarea_field($optimization['optimized_data']['development_guidelines'] ?? ''),
555 'ai_context' => sanitize_textarea_field($optimization['optimized_data']['ai_context'] ?? ''),
556 ],
557 'suggestions' => array_map('sanitize_text_field', $optimization['suggestions'] ?? []),
558 'tokens_used' => ($response['usage']['input_tokens'] ?? 0) + ($response['usage']['output_tokens'] ?? 0),
559 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
560 ];
561 }
562
563 /**
564 * Parse homepage meta optimization response
565 *
566 * @param array $response Claude API response
567 * @return array Parsed optimization data
568 * @throws \Exception If parsing fails
569 */
570 private function parse_homepage_meta_response(array $response): array {
571 if (!isset($response['content'][0]['text'])) {
572 throw new \Exception('Invalid response format from Claude');
573 }
574
575 $content = trim($response['content'][0]['text']);
576 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
577
578 // Extract JSON from response
579 $json_start = strpos($content, '{');
580 $json_end = strrpos($content, '}');
581
582 if (false === $json_start || false === $json_end) {
583 throw new \Exception('No valid JSON found in response');
584 }
585
586 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
587 $optimization = json_decode($json_content, true);
588
589 if (json_last_error() !== JSON_ERROR_NONE) {
590 throw new \Exception('Failed to parse JSON response: ' . esc_html(json_last_error_msg()));
591 }
592
593 // Validate and sanitize response
594 return [
595 'optimized_data' => [
596 'title' => sanitize_text_field($optimization['optimized_data']['title'] ?? ''),
597 'meta_description' => sanitize_text_field($optimization['optimized_data']['meta_description'] ?? ''),
598 ],
599 'analysis' => sanitize_textarea_field($optimization['analysis'] ?? ''),
600 'suggestions' => array_map('sanitize_text_field', $optimization['suggestions'] ?? []),
601 'score' => min(100, max(0, (int) ($optimization['score'] ?? 0))),
602 'tokens_used' => ($response['usage']['input_tokens'] ?? 0) + ($response['usage']['output_tokens'] ?? 0),
603 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
604 ];
605 }
606
607 /**
608 * Parse homepage hero optimization response
609 *
610 * @param array $response Claude API response
611 * @return array Parsed optimization data
612 * @throws \Exception If parsing fails
613 */
614 private function parse_homepage_hero_response(array $response): array {
615 if (!isset($response['content'][0]['text'])) {
616 throw new \Exception('Invalid response format from Claude');
617 }
618
619 $content = trim($response['content'][0]['text']);
620 $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)
621
622 // Extract JSON from response
623 $json_start = strpos($content, '{');
624 $json_end = strrpos($content, '}');
625
626 if (false === $json_start || false === $json_end) {
627 throw new \Exception('No valid JSON found in response');
628 }
629
630 $json_content = substr($content, $json_start, $json_end - $json_start + 1);
631 $optimization = json_decode($json_content, true);
632
633 if (json_last_error() !== JSON_ERROR_NONE) {
634 throw new \Exception('Failed to parse JSON response: ' . esc_html(json_last_error_msg()));
635 }
636
637 // Validate and sanitize response
638 return [
639 'optimized_data' => [
640 'hero_title' => sanitize_text_field($optimization['optimized_data']['hero_title'] ?? ''),
641 'hero_subtitle' => sanitize_text_field($optimization['optimized_data']['hero_subtitle'] ?? ''),
642 'hero_cta_text' => sanitize_text_field($optimization['optimized_data']['hero_cta_text'] ?? '')
643 ],
644 'analysis' => sanitize_textarea_field($optimization['analysis'] ?? ''),
645 'suggestions' => array_map('sanitize_text_field', $optimization['suggestions'] ?? []),
646 'score' => min(100, max(0, (int) ($optimization['score'] ?? 0))),
647 'tokens_used' => ($response['usage']['input_tokens'] ?? 0) + ($response['usage']['output_tokens'] ?? 0),
648 '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
649 ];
650 }
651 }
652