From 671c0a207a28fb41d24ed0ccd5f4ad8d392451b2 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Fri, 17 Apr 2026 02:41:45 +0700 Subject: [PATCH] =?UTF-8?q?refactor:=20extract=20PSA=5FAI=5FContent=20from?= =?UTF-8?q?=20PSA=5FAI=5FGenerator=20(539=E2=86=92282+228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt templates (validation + generation), meta field saving, category term assignment, and amino acid count derivation moved to PSA_AI_Content. Generator retains orchestration: validate, generate, background processing, dry-run, settings, input validation. Backward-compatible proxy methods on PSA_AI_Generator. Co-Authored-By: Claude Opus 4.6 --- includes/class-psa-ai-content.php | 228 +++++++++++++++++ includes/class-psa-ai-generator.php | 383 +++++----------------------- peptide-search-ai.php | 1 + 3 files changed, 292 insertions(+), 320 deletions(-) create mode 100644 includes/class-psa-ai-content.php diff --git a/includes/class-psa-ai-content.php b/includes/class-psa-ai-content.php new file mode 100644 index 0000000..34c3e7d --- /dev/null +++ b/includes/class-psa-ai-content.php @@ -0,0 +1,228 @@ + $ai_data['sequence'] ?? '', + 'psa_molecular_weight' => ! empty( $pubchem_data['molecular_weight'] ) ? $pubchem_data['molecular_weight'] : ( $ai_data['molecular_weight'] ?? '' ), + 'psa_molecular_formula' => ! empty( $pubchem_data['molecular_formula'] ) ? $pubchem_data['molecular_formula'] : ( $ai_data['molecular_formula'] ?? '' ), + 'psa_aliases' => $ai_data['aliases'] ?? '', + 'psa_mechanism' => $ai_data['mechanism'] ?? '', + 'psa_overview' => $ai_data['overview'] ?? $ai_data['description'] ?? '', + 'psa_category_label' => $ai_data['category_label'] ?? '', + 'psa_origin' => $ai_data['origin'] ?? '', + 'psa_research_apps' => $ai_data['research_benefits'] ?? ( $ai_data['research_applications'] ?? '' ), + 'psa_safety_profile' => $ai_data['safety_side_effects'] ?? ( $ai_data['safety_profile'] ?? '' ), + 'psa_dosage_info' => $ai_data['administration_dosing'] ?? ( $ai_data['dosage_info'] ?? '' ), + 'psa_legal_regulatory' => $ai_data['legal_regulatory'] ?? '', + 'psa_references' => $ai_data['references'] ?? '', + 'psa_category' => $ai_data['category'] ?? '', + 'psa_source' => ! empty( $pubchem_data ) ? 'pubchem' : 'ai-generated', + 'psa_pubchem_cid' => $pubchem_data['cid'] ?? '', + 'psa_half_life' => $ai_data['half_life'] ?? '', + 'psa_stability' => $ai_data['stability'] ?? '', + 'psa_solubility' => $ai_data['solubility'] ?? '', + 'psa_vial_size_mg' => $ai_data['vial_size_mg'] ?? '', + 'psa_storage_lyophilized' => $ai_data['storage_lyophilized'] ?? '', + 'psa_storage_reconstituted' => $ai_data['storage_reconstituted'] ?? '', + 'psa_typical_dose_mcg' => $ai_data['typical_dose_mcg'] ?? '', + 'psa_cycle_parameters' => $ai_data['cycle_parameters'] ?? '', + 'psa_amino_acid_count' => $amino_acid_count, + ); + + $meta = apply_filters( 'psa_peptide_meta', $meta, $post_id, $ai_data, $pubchem_data ); + + foreach ( $meta as $key => $value ) { + update_post_meta( $post_id, $key, $value ); + } + } + + /** + * Auto-assign a peptide_category taxonomy term based on AI-returned category. + * + * @param int $post_id The peptide post ID. + * @param array $ai_data AI-generated data containing 'category' or 'category_label'. + */ + public static function assign_category_term( int $post_id, array $ai_data ): void { + $ai_category = $ai_data['category'] ?? $ai_data['category_label'] ?? ''; + if ( empty( $ai_category ) ) { + return; + } + + // Why: AI returns varied labels ("GH Secretagogues", "Healing & Repair") that need + // to map to our controlled vocabulary of taxonomy terms. + $category_map = array( + 'gh secretagogues' => 'growth-hormone', + 'growth hormone' => 'growth-hormone', + 'healing & repair' => 'tissue-repair', + 'healing and repair' => 'tissue-repair', + 'tissue repair' => 'tissue-repair', + 'tissue healing' => 'tissue-repair', + 'cytoprotective' => 'tissue-repair', + 'melanocortin peptides' => 'dermatological', + 'melanocortin' => 'dermatological', + 'dermatological' => 'dermatological', + 'metabolic & anti-aging' => 'metabolic', + 'metabolic and anti-aging' => 'metabolic', + 'metabolic' => 'metabolic', + 'anti-aging' => 'aging-research', + 'aging research' => 'aging-research', + 'nootropic & neuroprotective' => 'immunology', + 'nootropic' => 'immunology', + 'neuroprotective' => 'immunology', + 'immunology' => 'immunology', + 'immune' => 'immunology', + 'lipid metabolism' => 'lipid-metabolism', + 'endocrine' => 'endocrine', + ); + + $lower = strtolower( trim( $ai_category ) ); + $slug = $category_map[ $lower ] ?? ''; + + // Fuzzy fallback: check if AI category contains any known slug keyword. + if ( empty( $slug ) ) { + foreach ( PSA_Post_Type::DEFAULT_CATEGORIES as $cat_slug => $cat_name ) { + if ( stripos( $ai_category, $cat_name ) !== false || stripos( $ai_category, str_replace( '-', ' ', $cat_slug ) ) !== false ) { + $slug = $cat_slug; + break; + } + } + } + + if ( ! empty( $slug ) ) { + wp_set_object_terms( $post_id, $slug, 'peptide_category' ); + } + } + + /** + * Derive amino acid count from sequence data or explicit AI field. + * + * @param array $ai_data AI-generated data. + * @return string Amino acid count as string, or empty. + */ + private static function derive_amino_acid_count( array $ai_data ): string { + $sequence = $ai_data['sequence'] ?? ''; + $count = ''; + + if ( ! empty( $sequence ) ) { + // Three-letter codes use dashes; single-letter codes are uppercase chars. + $count = ( strpos( $sequence, '-' ) !== false ) + ? (string) count( explode( '-', $sequence ) ) + : (string) strlen( preg_replace( '/[^A-Z]/', '', $sequence ) ); + } + + // AI may return this directly — prefer explicit value. + if ( ! empty( $ai_data['amino_acid_count'] ) ) { + $count = (string) intval( $ai_data['amino_acid_count'] ); + } + + return $count; + } +} diff --git a/includes/class-psa-ai-generator.php b/includes/class-psa-ai-generator.php index 46339c2..7e683c9 100755 --- a/includes/class-psa-ai-generator.php +++ b/includes/class-psa-ai-generator.php @@ -1,16 +1,18 @@ false, - 'canonical_name' => $name, - 'reason' => 'Invalid characters in peptide name.', - ); + return array( 'is_valid' => false, 'canonical_name' => $name, 'reason' => 'Invalid characters in peptide name.' ); } $options = self::get_settings(); - $api_key = $options['api_key']; - $model = $options['validation_model']; - - if ( empty( $api_key ) ) { + if ( empty( $options['api_key'] ) ) { return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' ); } - - // Check monthly budget before validation. if ( PSA_Cost_Tracker::is_budget_exceeded() ) { return new WP_Error( 'budget_exceeded', 'Monthly API budget reached.' ); } - // Check transient cache first (avoid repeated API calls for same term). + // Transient cache to avoid repeated API calls for same term. $cache_key = 'psa_validate_' . md5( strtolower( trim( $name ) ) ); $cached = get_transient( $cache_key ); if ( false !== $cached ) { return $cached; } - $prompt = self::build_validation_prompt( $name ); - // Allow filtering of validation prompt before sending. - $prompt = apply_filters( 'psa_validation_prompt', $prompt, $name ); + $prompt = PSA_AI_Content::build_validation_prompt( $name ); + $prompt = apply_filters( 'psa_validation_prompt', $prompt, $name ); $response = PSA_OpenRouter::send_request( - $api_key, - $model ? $model : 'google/gemini-2.0-flash-001', + $options['api_key'], + $options['validation_model'] ? $options['validation_model'] : 'google/gemini-2.0-flash-001', $prompt, PSA_Config::VALIDATION_MAX_TOKENS, 'validation', @@ -79,11 +70,7 @@ class PSA_AI_Generator { $data = PSA_OpenRouter::parse_response( $response ); if ( is_wp_error( $data ) ) { - return array( - 'is_valid' => false, - 'canonical_name' => $name, - 'reason' => 'Could not verify this peptide name.', - ); + return array( 'is_valid' => false, 'canonical_name' => $name, 'reason' => 'Could not verify this peptide name.' ); } $result = array( @@ -92,9 +79,7 @@ class PSA_AI_Generator { 'reason' => sanitize_text_field( $data['reason'] ?? '' ), ); - // Cache for 24 hours (using PSA_Config for consistency). set_transient( $cache_key, $result, DAY_IN_SECONDS ); - return $result; } @@ -111,43 +96,34 @@ class PSA_AI_Generator { return; } - // Only process posts that are still in 'pending' state. $source = get_post_meta( $post_id, 'psa_source', true ); if ( 'pending' !== $source ) { error_log( 'PSA: background_generate skipped — post ' . $post_id . ' source is "' . $source . '", not "pending".' ); return; } - $options = self::get_settings(); - $auto_publish = $options['auto_publish']; - + $options = self::get_settings(); error_log( 'PSA: Generating content for "' . $peptide_name . '" (post ' . $post_id . ') using model: ' . ( $options['ai_model'] ? $options['ai_model'] : 'google/gemini-2.5-flash' ) ); - // Allow developers to hook before generation starts. do_action( 'psa_before_generation', $post_id, $peptide_name ); - // Generate the full content. $result = self::generate_peptide_content( $peptide_name ); if ( is_wp_error( $result ) ) { - $err_msg = $result->get_error_message(); - error_log( 'PSA: Generation FAILED for "' . $peptide_name . '": ' . $err_msg ); - update_post_meta( $post_id, 'psa_generation_error', $err_msg ); + error_log( 'PSA: Generation FAILED for "' . $peptide_name . '": ' . $result->get_error_message() ); + update_post_meta( $post_id, 'psa_generation_error', $result->get_error_message() ); return; } error_log( 'PSA: AI content generated successfully for "' . $peptide_name . '". Fields: ' . implode( ', ', array_keys( $result ) ) ); - // FIX: Use 'overview' field (which the prompt returns) with fallback to 'description'. $post_content = $result['overview'] ?? $result['description'] ?? ''; - - // Safeguard: if post_content is still empty after generation, log a warning. if ( empty( trim( $post_content ) ) ) { - error_log( 'PSA: WARNING — AI returned valid JSON but post content (overview/description) is empty for "' . $peptide_name . '". Available keys: ' . implode( ', ', array_keys( $result ) ) ); - update_post_meta( $post_id, 'psa_generation_error', 'AI returned empty overview/description content. Available fields: ' . implode( ', ', array_keys( $result ) ) ); + error_log( 'PSA: WARNING — AI returned valid JSON but overview/description is empty for "' . $peptide_name . '".' ); + update_post_meta( $post_id, 'psa_generation_error', 'AI returned empty overview/description. Fields: ' . implode( ', ', array_keys( $result ) ) ); return; } - // Try PubChem enrichment. + // PubChem enrichment for molecular data. $pubchem_data = null; if ( ! empty( $options['use_pubchem'] ) ) { $pubchem_result = PSA_PubChem::lookup( $peptide_name ); @@ -159,18 +135,13 @@ class PSA_AI_Generator { } } - // Update the placeholder with real content. - $post_status = ( 'publish' === $auto_publish ) ? 'publish' : 'draft'; - - $update_result = wp_update_post( - array( - 'ID' => $post_id, - 'post_title' => sanitize_text_field( $result['name'] ?? $peptide_name ), - 'post_content' => wp_kses_post( $post_content ), - 'post_status' => $post_status, - ), - true - ); + $post_status = ( 'publish' === $options['auto_publish'] ) ? 'publish' : 'draft'; + $update_result = wp_update_post( array( + 'ID' => $post_id, + 'post_title' => sanitize_text_field( $result['name'] ?? $peptide_name ), + 'post_content' => wp_kses_post( $post_content ), + 'post_status' => $post_status, + ), true ); if ( is_wp_error( $update_result ) ) { error_log( 'PSA: wp_update_post FAILED for post ' . $post_id . ': ' . $update_result->get_error_message() ); @@ -178,23 +149,15 @@ class PSA_AI_Generator { return; } - // Save all meta fields — prefer PubChem data for molecular properties. - self::save_peptide_meta( $post_id, $result, $pubchem_data ); - - // Auto-assign peptide_category taxonomy term based on AI category. - self::assign_category_term( $post_id, $result ); - - // Create a matching Knowledge Base article. + PSA_AI_Content::save_peptide_meta( $post_id, $result, $pubchem_data ); + PSA_AI_Content::assign_category_term( $post_id, $result ); PSA_KB_Builder::create_article( $post_id, $result, $peptide_name, $post_status ); - // Clean up generation tracking. delete_post_meta( $post_id, 'psa_generation_started' ); delete_post_meta( $post_id, 'psa_generation_error' ); update_post_meta( $post_id, 'psa_generation_completed', time() ); - // Allow developers to hook after generation completes. do_action( 'psa_after_generation', $post_id, $result, $peptide_name ); - error_log( 'PSA: Successfully completed generation for "' . $peptide_name . '" (post ' . $post_id . ', status: ' . $post_status . ')' ); } @@ -206,24 +169,18 @@ class PSA_AI_Generator { */ public static function generate_peptide_content( string $peptide_name ) { $options = self::get_settings(); - $api_key = $options['api_key']; - $model = $options['ai_model']; - - if ( empty( $api_key ) ) { + if ( empty( $options['api_key'] ) ) { return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' ); } - - // Check monthly budget before generation. if ( PSA_Cost_Tracker::is_budget_exceeded() ) { return new WP_Error( 'budget_exceeded', 'Monthly API budget reached.' ); } - $prompt = self::build_generation_prompt( $peptide_name ); - // Allow filtering of generation prompt before sending. - $prompt = apply_filters( 'psa_generation_prompt', $prompt, $peptide_name ); + $prompt = PSA_AI_Content::build_generation_prompt( $peptide_name ); + $prompt = apply_filters( 'psa_generation_prompt', $prompt, $peptide_name ); $response = PSA_OpenRouter::send_request( - $api_key, - $model ? $model : 'google/gemini-2.5-flash', + $options['api_key'], + $options['ai_model'] ? $options['ai_model'] : 'google/gemini-2.5-flash', $prompt, PSA_Config::GENERATION_MAX_TOKENS, 'generation', @@ -238,64 +195,42 @@ class PSA_AI_Generator { } /** - * Perform a dry-run to estimate costs WITHOUT making API calls. + * Dry-run: estimate costs without making API calls. * * @param string $peptide_name The peptide name to estimate. - * @return array { - * 'validation' => { tokens: int, estimated_cost_usd: float }, - * 'generation' => { tokens: int, estimated_cost_usd: float }, - * 'total_estimated_cost_usd' => float, - * } + * @return array { validation: {tokens, cost}, generation: {tokens, cost}, total } */ public static function dry_run( string $peptide_name ): array { $options = self::get_settings(); $validation_model = $options['validation_model'] ? $options['validation_model'] : 'google/gemini-2.0-flash-001'; $generation_model = $options['ai_model'] ? $options['ai_model'] : 'google/gemini-2.5-flash'; - $validation_prompt = self::build_validation_prompt( $peptide_name ); - $validation_prompt_tokens = (int) ceil( strlen( $validation_prompt ) / 4 ); - $validation_completion = PSA_Config::VALIDATION_MAX_TOKENS; - $validation_cost = PSA_Cost_Tracker::estimate_cost( $validation_model, $validation_prompt_tokens, $validation_completion ); + $val_prompt = PSA_AI_Content::build_validation_prompt( $peptide_name ); + $val_tokens = (int) ceil( strlen( $val_prompt ) / 4 ); + $val_cost = PSA_Cost_Tracker::estimate_cost( $validation_model, $val_tokens, PSA_Config::VALIDATION_MAX_TOKENS ); - $generation_prompt = self::build_generation_prompt( $peptide_name ); - $generation_prompt_tokens = (int) ceil( strlen( $generation_prompt ) / 4 ); - $generation_completion = PSA_Config::GENERATION_MAX_TOKENS; - $generation_cost = PSA_Cost_Tracker::estimate_cost( $generation_model, $generation_prompt_tokens, $generation_completion ); + $gen_prompt = PSA_AI_Content::build_generation_prompt( $peptide_name ); + $gen_tokens = (int) ceil( strlen( $gen_prompt ) / 4 ); + $gen_cost = PSA_Cost_Tracker::estimate_cost( $generation_model, $gen_tokens, PSA_Config::GENERATION_MAX_TOKENS ); return array( - 'validation' => array( - 'tokens' => $validation_prompt_tokens + $validation_completion, - 'estimated_cost_usd' => $validation_cost, - ), - 'generation' => array( - 'tokens' => $generation_prompt_tokens + $generation_completion, - 'estimated_cost_usd' => $generation_cost, - ), - 'total_estimated_cost_usd' => $validation_cost + $generation_cost, + 'validation' => array( 'tokens' => $val_tokens + PSA_Config::VALIDATION_MAX_TOKENS, 'estimated_cost_usd' => $val_cost ), + 'generation' => array( 'tokens' => $gen_tokens + PSA_Config::GENERATION_MAX_TOKENS, 'estimated_cost_usd' => $gen_cost ), + 'total_estimated_cost_usd' => $val_cost + $gen_cost, ); } - // ------------------------------------------------------------------------- - // Private helpers - // ------------------------------------------------------------------------- + // ── Private helpers ───────────────────────────────────────────────── /** - * Get plugin settings with defaults. + * Get plugin settings with defaults, decrypted API key, and constant override. * * @return array */ private static function get_settings(): array { - $defaults = array( - 'api_key' => '', - 'ai_model' => '', - 'validation_model' => '', - 'auto_publish' => 'draft', - 'use_pubchem' => '1', - ); - + $defaults = array( 'api_key' => '', 'ai_model' => '', 'validation_model' => '', 'auto_publish' => 'draft', 'use_pubchem' => '1' ); $settings = wp_parse_args( get_option( 'psa_settings', array() ), $defaults ); - // Decrypt API key if it's encrypted and constant is not defined. if ( ! empty( $settings['api_key'] ) && ! ( defined( 'PSA_OPENROUTER_KEY' ) && PSA_OPENROUTER_KEY ) ) { $decrypted = PSA_Encryption::decrypt( $settings['api_key'] ); if ( false !== $decrypted ) { @@ -303,139 +238,12 @@ class PSA_AI_Generator { } } - // Prefer wp-config.php constant over database value for security. + // wp-config.php constant takes precedence for security. if ( defined( 'PSA_OPENROUTER_KEY' ) && PSA_OPENROUTER_KEY ) { $settings['api_key'] = PSA_OPENROUTER_KEY; } - $settings = apply_filters( 'psa_ai_settings', $settings ); - - return $settings; - } - - /** - * Save meta fields for a peptide post (core + extended). - * - * @param int $post_id The post ID. - * @param array $ai_data AI-generated data. - * @param array|null $pubchem_data PubChem data (or null). - */ - private static function save_peptide_meta( int $post_id, array $ai_data, ?array $pubchem_data ): void { - // Derive amino acid count from sequence if available. - $amino_acid_count = ''; - $sequence = $ai_data['sequence'] ?? ''; - if ( ! empty( $sequence ) ) { - // Count residues: split on dashes for three-letter codes, or count chars for single-letter. - if ( strpos( $sequence, '-' ) !== false ) { - $amino_acid_count = (string) count( explode( '-', $sequence ) ); - } else { - $amino_acid_count = (string) strlen( preg_replace( '/[^A-Z]/', '', $sequence ) ); - } - } - // AI may also return this directly. - if ( ! empty( $ai_data['amino_acid_count'] ) ) { - $amino_acid_count = (string) intval( $ai_data['amino_acid_count'] ); - } - - $meta = array( - 'psa_sequence' => $ai_data['sequence'] ?? '', - 'psa_molecular_weight' => ! empty( $pubchem_data['molecular_weight'] ) - ? $pubchem_data['molecular_weight'] - : ( $ai_data['molecular_weight'] ?? '' ), - 'psa_molecular_formula' => ! empty( $pubchem_data['molecular_formula'] ) - ? $pubchem_data['molecular_formula'] - : ( $ai_data['molecular_formula'] ?? '' ), - 'psa_aliases' => $ai_data['aliases'] ?? '', - 'psa_mechanism' => $ai_data['mechanism'] ?? '', - 'psa_overview' => $ai_data['overview'] ?? $ai_data['description'] ?? '', - 'psa_category_label' => $ai_data['category_label'] ?? '', - 'psa_origin' => $ai_data['origin'] ?? '', - 'psa_research_apps' => $ai_data['research_benefits'] ?? ( $ai_data['research_applications'] ?? '' ), - 'psa_safety_profile' => $ai_data['safety_side_effects'] ?? ( $ai_data['safety_profile'] ?? '' ), - 'psa_dosage_info' => $ai_data['administration_dosing'] ?? ( $ai_data['dosage_info'] ?? '' ), - 'psa_legal_regulatory' => $ai_data['legal_regulatory'] ?? '', - 'psa_references' => $ai_data['references'] ?? '', - 'psa_category' => $ai_data['category'] ?? '', - 'psa_source' => ! empty( $pubchem_data ) ? 'pubchem' : 'ai-generated', - 'psa_pubchem_cid' => $pubchem_data['cid'] ?? '', - // Extended fields (v4.3.0). - 'psa_half_life' => $ai_data['half_life'] ?? '', - 'psa_stability' => $ai_data['stability'] ?? '', - 'psa_solubility' => $ai_data['solubility'] ?? '', - 'psa_vial_size_mg' => $ai_data['vial_size_mg'] ?? '', - 'psa_storage_lyophilized' => $ai_data['storage_lyophilized'] ?? '', - 'psa_storage_reconstituted' => $ai_data['storage_reconstituted'] ?? '', - 'psa_typical_dose_mcg' => $ai_data['typical_dose_mcg'] ?? '', - 'psa_cycle_parameters' => $ai_data['cycle_parameters'] ?? '', - 'psa_amino_acid_count' => $amino_acid_count, - ); - - $meta = apply_filters( 'psa_peptide_meta', $meta, $post_id, $ai_data, $pubchem_data ); - - foreach ( $meta as $key => $value ) { - update_post_meta( $post_id, $key, $value ); - } - } - - /** - * Auto-assign a peptide_category taxonomy term based on AI-returned category. - * - * Maps the AI's free-text category to the registered taxonomy terms. - * Falls back to fuzzy matching if no exact match found. - * - * @param int $post_id The peptide post ID. - * @param array $ai_data AI-generated data containing 'category' or 'category_label'. - */ - private static function assign_category_term( int $post_id, array $ai_data ): void { - $ai_category = $ai_data['category'] ?? $ai_data['category_label'] ?? ''; - if ( empty( $ai_category ) ) { - return; - } - - // Mapping from AI free-text categories to taxonomy slugs. - // Why: AI returns varied labels ("GH Secretagogues", "Healing & Repair") that need - // to map to our controlled vocabulary of taxonomy terms. - $category_map = array( - 'gh secretagogues' => 'growth-hormone', - 'growth hormone' => 'growth-hormone', - 'healing & repair' => 'tissue-repair', - 'healing and repair' => 'tissue-repair', - 'tissue repair' => 'tissue-repair', - 'tissue healing' => 'tissue-repair', - 'cytoprotective' => 'tissue-repair', - 'melanocortin peptides' => 'dermatological', - 'melanocortin' => 'dermatological', - 'dermatological' => 'dermatological', - 'metabolic & anti-aging' => 'metabolic', - 'metabolic and anti-aging' => 'metabolic', - 'metabolic' => 'metabolic', - 'anti-aging' => 'aging-research', - 'aging research' => 'aging-research', - 'nootropic & neuroprotective' => 'immunology', - 'nootropic' => 'immunology', - 'neuroprotective' => 'immunology', - 'immunology' => 'immunology', - 'immune' => 'immunology', - 'lipid metabolism' => 'lipid-metabolism', - 'endocrine' => 'endocrine', - ); - - $lower = strtolower( trim( $ai_category ) ); - $slug = $category_map[ $lower ] ?? ''; - - // Fuzzy fallback: check if AI category contains any known slug keyword. - if ( empty( $slug ) ) { - foreach ( PSA_Post_Type::DEFAULT_CATEGORIES as $cat_slug => $cat_name ) { - if ( stripos( $ai_category, $cat_name ) !== false || stripos( $ai_category, str_replace( '-', ' ', $cat_slug ) ) !== false ) { - $slug = $cat_slug; - break; - } - } - } - - if ( ! empty( $slug ) ) { - wp_set_object_terms( $post_id, $slug, 'peptide_category' ); - } + return apply_filters( 'psa_ai_settings', $settings ); } /** @@ -458,82 +266,17 @@ class PSA_AI_Generator { return true; } - /** - * Build the lightweight validation prompt. - * - * @param string $name The peptide name to validate. - * @return string Prompt text ready to send to OpenRouter API. - */ - private static function build_validation_prompt( string $name ): string { - $json_name = wp_json_encode( $name ); - return <<