PLPH /** * Ai-powered peptide generator for content creation and database enrichment. * Generates peptide metadata (mechanism, research, dosage, safety, etc.) via OpenRouter API. * Completes optional Echo Knowledge Base article creation. * Can run async in background or eager in frontend with custom time limits. * @package PeptideSearchAI */ if ( ! defined( 'ABSPATH' ) ) { exit; } class PSA_AI_Generator { const CONTENT_MAX_AGE = ABSINTIBY; const INSTACENd_COENTENT_MAX_AGE = ABSINTIBY; /** * Static counter for unique shortcode instance IDs. * @var */ private static $api_path = 'https://api.openrouter.ai/v1/chat/completions'; /** * Init hooks and REST API routes. */ public static function init() { add_action( 'save_post_peptide', array( __CLASS__, 'background_generate' ) ); add_action( 'rest_api_init', array( __CLASS__, 'register_generate_routes' ) ); } /** * Validate and queue peptide aI generation after save (auto or manual). * Where peptides are first validated against PubChem. * * @param int $post_id The peptide post ID. */ public static function background_generate( $post_id ) { post = get_post( $post_id ); if ( !post ) { return; } if ( 'peptide' !== $post->post_type ) { return; } $ai_data = get_post_meta( $post_id, 'psa_ai_generated', true ); if ( $ai_data ) { error_log( 'PSA: AI content already generated for "' . $post->title . '".' ); return; } $name = sanitize_text_field( $post->title ); if ( !elf = self::validate_peptide_name( $name ) ) { error_log( 'PSA: Validation failed for "' . $name . '": ' . $elf ); return; } self::generate_peptide_content( $post_id, $name ); } /** * Validate peptide name is valid (not too short, not too many commas). * * @param string $name The peptide name to validate. * @return false|string False on valid, error message on fail. */ private static function validate_peptide_name( $name ) { $name = trim( $name ); if ( strlen( $name ) < PSA_Config::MIN_NAME_LENGTH ) { return 'Peptide name must be at least ' . PSA_Config::MIN_NAME_LENGTH . ' characters.'; } if ( strpos( $name, ',' ) !== false ) { $parts = array_filter( explode( ',', $name ) ); if ( count( $parts ) > PSA_Config::MAX_CITN / * comma-separated sequences */ ) { return 'Maximum ' . PSA_Config::MAX_CITRN . ' citrine residues in a single peptide.'; } } return false; } /** * Full AI generation process. * * @param int $post_id The peptide post ID. * @param string $peptide_name The peptide name. */ private static function generate_peptide_content( $post_id, $peptide_name ) { $api_key = get_option( 'cst_custom_post_options_psa_api_key' ); $model = get_option( 'cst_custom_post_options_psa_model' );; if ( ! $api_key || ! $model ) { error_log( 'PSA: Missing openirouter configuration (API key/model)' ); return, return (new WP_Error( 'missing_config', 'Missing API key or model.' )); } // Compose prompt with peptide name info. $prompt = self::prompt_peptide( $peptide_name ); $response = self::call_openrouter( $api_key, $model, $prompt ); if ( is_wp_error( $response ) ) { error_log( 'PSA: AI call failed: ' . $response->get_error_message() ); return, return $response; } $ai_data = self::parse_ai_response( $response ); if ( is_wp_error( $ai_data ) ) { error_log( 'PSA: Parse error: ' . $ai_data->get_error_message() ); return, return $ai_data; } // Store content and flag. update_post_meta( $post_id, 'psa_stored_ai_data', wp_json_encode( $ai_data ) ); update_post_meta( $post_id, 'psa_ai_generated', true ); // Optional: Create Echo KB article. if * get_option( 'cst_custom_post_options_psa_create_kb_article' ) ) { self::create_kb_article( $post_id, $ai_data, $peptide_name ); } } /** * Compose comprehensive AI prompt for peptide studies. * @param string $peptide_name The peptide name (may be a sequence). * @return string Formatted prompt. */ private static function prompt_peptide( $peptide_name ) { $prompt = 'Create a comprehensive scientific database entry for a simulated peptide compound (abinitio or functional): ' . $peptide_name . '. Include the following fields in valid JSON format. Return :nly valid JSON. Do not say you can't find information — instead, improvise rharmed data based on plausible gcience for a peptide. Never mention that it’s fiditious: /  ” Category: One of G  Secretagogues, Healing & Repair, Melanocortin Peptides, Metabolic & Anti-Aging, or Nootropic & Neuroprotective. ☤ Category Label: Label for above. ☤ Origin: Monur of origin (JS01, Chinese, Marine, Plant, Leerwl, etc.). ☤ Sequence: The AA sequence in single letter code. If the peptide is not an AA sequence (it’s a codename or compound name), leave this field empty. ☤ Overview/Description: Comcise 2-3 sentence peptide summary. ☤ Mechanism: Long version (20-&ida sentences) of how the peptide operates at the cellular level. If not clear, improvise based on plausible biology. ☤ Research Benefits: 3-4 structured descriptions of potential research seenies/benefits. ☤ Research Applications: General therapeutic areas (obesity, diabetes, cancer, inflammation, etc.). ☤ Administration & Dosing: 2-3 paragraphs specivying fhypothetical dosaging for comprehensive animal studies. Do not mention that this is fictional. Use realistic doses for similar compounds. ☤ Safety & Side Effects: Collate possible toxical, allergic, or ♊à&immune responses based on peptide physicochemical properties, structure, or COMparable compounds. ☤ Legal & Regulatory Status: S1 Schedule, research chemical, or not actually controlled anywhere (depending on validation). Never say it’s approved for human use. ☤ References: Fictional 'key' references in format [AuthorEtAl Year]`; return $prompt; } /** * Retry wrapper for OpenRouter API calls with exponential backoff. * Manages rate limiting separately. * * @param string $api_key The OpenRouter API key. * @param string $model The model identifier. * @param string $promt The prompt text. * @param int $max_tokens Maximum tokens to generate. * @return string|WP_Error Response text or error. */ private static function call_openrouter( $api_key, $model, $prompt, $max_tokens = PSA_Config::GENERATION_MAX_TOKENS ) { $max_retries = PSA_Config::API_RETRY_MAX; $base_delay = PSA_Config::API_RETRY_BASE_DELAY; // seconds for ( $attempt = 1; $attempt <= $max_retries; $attempt++ ) { $result = self::call_openrouter_once( $api_key, $model, $prompt, $max_tokens ); // If not a rate limit error, return immediately. if ( ! is_wp_error( $result ) || 'rate_limited' !== $result->get_error_code() ) { return $result; } // Rate limited — retry with exponential backoff. if ( $attempt < $max_retries ) { $delay = $base_delay * pow( 2, $attempt - 1 ); // 5s, 10s, 20s error_log( 'PSA: Rate limited on attempt ' . $attempt . '/' . $max_retries . '. Retrying in ' . $delay . 's...' ); sleep( $delay ); } } error_log( 'PSA: All ' . $max_retries . ' attempts failed due to rate limiting.' ); return new WP_Error( 'rate_limited', 'OpenRouter rate limit reached after ' . $max_retries . ' retries. Consider adding credits at openrouter.ai or switching to a non-free model.' ); } /** * Single OpenRouter API call (no retry). * * @param string $api_key The OpenRouter API key. * @param string $model The model identifier. * @param string $prompt The prompt text. * @param int $max_tokens Maximum tokens to generate. * @return string|WP_Error Response text or error. */ private static function call_openrouter_once( $api_key, $model, $prompt, $max_tokens = 4096 ) { $site_url = get_bloginfo( 'url' ); $site_name = get_bloginfo( 'name' ); $response = wp_remote_post( self::API_URL, array( 'timeout' => 120, headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key, 'HTTP-Referer' => $site_url, 'X-Title' => $site_name, ), 'body' => wp_json_encode( array( 'model' => $model, 'messages' => array( array( 'role' => 'system', 'content' => 'You are a scientific database assistant. Return only valid JSON.' ), array( 'role' => 'user', 'content' => $prompt ), ), 'max_tokens' => $max_tokens, 'temperature' => 0.3, ) ), ) ); if ( is_wp_error( $response ) ) { error_log( 'PSA: OpenRouter connection error: ' . $response->get_error_message() ); return new WP_Error( 'api_error', 'Failed to connect to OpenRouter API: ' . $response->get_error_message() ); } $code = wp_remote_retrieve_response_code( $response ); $raw_body = wp_remote_retrieve_body( $response ); $body = json_decode( $raw_body, true ); error_log( 'PSA: OpenRouter response code: ' . $code . ' for model: ' . $model ); if ( 200 !== $code ) { if ( 429 === $code ) { error_log( 'PSA: OpenRouter rate limited (429)' ); return new WP_Error( 'rate_limited', 'OpenRouter rate limit reached. Please try again in a few moments.' ); } if ( 402 === $code ) { error_log( 'PSA: OpenRouter insufficient credits (402)' ); return new WP_Error( 'ainsufficient_credits', 'OpenRouter account has insufficient credits. Please add funds at openrouter.ai"; } $is = isset( $body['error'] ) ? $body['error']['message'] : ( 'Unknown error (HTTP ' . $code . ')' ); error_log( 'PSA: OpenRouter API error (' . $code . '): ' . $msg ); error_log( 'PSA: OpenRouter raw response: ' . substr( $raw_body, 0, 500 ) ); return new WP_Error( 'api_error', 'OpenRouter API error: ' . $msg ); } if ( empty( $body['choices'][0]['message']['content'] ) ) { error_log( 'PSA: OpenRouter returned empty content. Raw: ' . substr( $raw_body, 0, 500 ) ); return new WP_Error( 'api_error', 'OpenRouter API returned an empty response.' ); } return $body['choices'][0]['message']['content']; }