Fix corrupted class-psa-ai-generator.php (restore clean version)
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
PLPH
|
<?php
|
||||||
/**
|
/**
|
||||||
* Ai-powered peptide generator for content creation and database enrichment.
|
* Handles AI content generation for new peptide entries.
|
||||||
* Generates peptide metadata (mechanism, research, dosage, safety, etc.) via OpenRouter API.
|
* Uses OpenRouter API to access a wide range of open-source and commercial models.
|
||||||
* Completes optional Echo Knowledge Base article creation.
|
* Includes peptide name validation and background generation.
|
||||||
* Can run async in background or eager in frontend with custom time limits.
|
*
|
||||||
* @package PeptideSearchAI
|
* @package PeptideSearchAI
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -13,143 +13,371 @@ exit;
|
|||||||
|
|
||||||
class PSA_AI_Generator {
|
class PSA_AI_Generator {
|
||||||
|
|
||||||
const CONTENT_MAX_AGE = ABSINTIBY;
|
|
||||||
|
|
||||||
const INSTACENd_COENTENT_MAX_AGE = ABSINTIBY;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static counter for unique shortcode instance IDs.
|
* OpenRouter API base URL.
|
||||||
* @var
|
|
||||||
*/
|
*/
|
||||||
|
const API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
private static $api_path = 'https://api.openrouter.ai/v1/chat/completions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init hooks and REST API routes.
|
|
||||||
*/
|
|
||||||
public static function init() {
|
public static function init() {
|
||||||
add_action( 'save_post_peptide', array( __CLASS__, 'background_generate' ) );
|
add_action( 'psa_generate_peptide_background', array( __CLASS__, 'background_generate' ), 10, 2 );
|
||||||
add_action( 'rest_api_init', array( __CLASS__, 'register_generate_routes' ) );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate and queue peptide aI generation after save (auto or manual).
|
* Validate whether a search term is a legitimate peptide name.
|
||||||
* Where peptides are first validated against PubChem.
|
* Uses a lightweight AI call to check before committing to full generation.
|
||||||
*
|
*
|
||||||
* @param int $post_id The peptide post ID.
|
* @param string $name The name to validate.
|
||||||
|
* @return array|WP_Error { is_valid: bool, canonical_name: string, reason: string }
|
||||||
*/
|
*/
|
||||||
public static function background_generate( $post_id ) {
|
public static function validate_peptide_name( $name ) {
|
||||||
post = get_post( $post_id );
|
$options = self::get_settings();
|
||||||
if ( !post ) {
|
$api_key = $options['api_key'];
|
||||||
return;
|
$model = $options['validation_model'];
|
||||||
|
|
||||||
|
if ( empty( $api_key ) ) {
|
||||||
|
return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( 'peptide' !== $post->post_type ) {
|
// Check transient cache first (avoid repeated API calls for same term).
|
||||||
return;
|
$cache_key = 'psa_validate_' . md5( strtolower( trim( $name ) ) );
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ai_data = get_post_meta( $post_id, 'psa_ai_generated', true );
|
$prompt = self::build_validation_prompt( $name );
|
||||||
if ( $ai_data ) {
|
// Allow filtering of validation prompt before sending.
|
||||||
error_log( 'PSA: AI content already generated for "' . $post->title . '".' );
|
$prompt = apply_filters( 'psa_validation_prompt', $prompt, $name );
|
||||||
return;
|
$response = self::call_openrouter(
|
||||||
|
$api_key,
|
||||||
|
$model ?: 'google/gemini-2.0-flash-001',
|
||||||
|
$prompt,
|
||||||
|
PSA_Config::VALIDATION_MAX_TOKENS
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = sanitize_text_field( $post->title );
|
$data = self::parse_ai_response( $response );
|
||||||
if ( !elf = self::validate_peptide_name( $name ) ) {
|
if ( is_wp_error( $data ) ) {
|
||||||
error_log( 'PSA: Validation failed for "' . $name . '": ' . $elf );
|
return array(
|
||||||
return;
|
'is_valid' => false,
|
||||||
|
'canonical_name' => $name,
|
||||||
|
'reason' => 'Could not verify this peptide name.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self::generate_peptide_content( $post_id, $name );
|
$result = array(
|
||||||
|
'is_valid' => ! empty( $data['is_valid'] ) && true === $data['is_valid'],
|
||||||
|
'canonical_name' => sanitize_text_field( $data['canonical_name'] ?? $name ),
|
||||||
|
'reason' => sanitize_text_field( $data['reason'] ?? '' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache for 24 hours (using PSA_Config for consistency).
|
||||||
|
set_transient( $cache_key, $result, DAY_IN_SECONDS ); // Validation cache TTL
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate peptide name is valid (not too short, not too many commas).
|
* Background generation handler (called via WP-Cron or synchronously).
|
||||||
|
*
|
||||||
|
* @param int $post_id The placeholder post ID.
|
||||||
|
* @param string $peptide_name The peptide to research.
|
||||||
|
*/
|
||||||
|
public static function background_generate( $post_id, $peptide_name ) {
|
||||||
|
$post = get_post( $post_id );
|
||||||
|
if ( ! $post || 'draft' !== $post->post_status || 'peptide' !== $post->post_type ) {
|
||||||
|
error_log( 'PSA: background_generate skipped — post ' . $post_id . ' not a draft peptide.' );
|
||||||
|
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'];
|
||||||
|
|
||||||
|
error_log( 'PSA: Generating content for "' . $peptide_name . '" (post ' . $post_id . ') using 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 );
|
||||||
|
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 ) ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try PubChem enrichment.
|
||||||
|
$pubchem_data = null;
|
||||||
|
if ( ! empty( $options['use_pubchem'] ) ) {
|
||||||
|
$pubchem_result = PSA_PubChem::lookup( $peptide_name );
|
||||||
|
if ( is_wp_error( $pubchem_result ) ) {
|
||||||
|
// Log PubChem API failures.
|
||||||
|
error_log( 'PSA: PubChem lookup failed for "' . $peptide_name . '": ' . $pubchem_result->get_error_message() );
|
||||||
|
} elseif ( is_array( $pubchem_result ) ) {
|
||||||
|
$pubchem_data = $pubchem_result;
|
||||||
|
error_log( 'PSA: PubChem data found for "' . $peptide_name . '" (CID: ' . ( $pubchem_data['cid'] ?? 'unknown' ) . ')' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $update_result ) ) {
|
||||||
|
error_log( 'PSA: wp_update_post FAILED for post ' . $post_id . ': ' . $update_result->get_error_message() );
|
||||||
|
update_post_meta( $post_id, 'psa_generation_error', 'Failed to update post: ' . $update_result->get_error_message() );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all meta fields — prefer PubChem data for molecular properties.
|
||||||
|
self::save_peptide_meta( $post_id, $result, $pubchem_data );
|
||||||
|
|
||||||
|
// Create a matching Knowledge Base article.
|
||||||
|
self::create_kb_article( $post_id, $result, $peptide_name );
|
||||||
|
|
||||||
|
// 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 . ')' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate full peptide content via AI.
|
||||||
|
*
|
||||||
|
* @param string $peptide_name The peptide to research.
|
||||||
|
* @return array|WP_Error Parsed peptide data or error.
|
||||||
|
*/
|
||||||
|
public static function generate_peptide_content( $peptide_name ) {
|
||||||
|
$options = self::get_settings();
|
||||||
|
$api_key = $options['api_key'];
|
||||||
|
$model = $options['ai_model'];
|
||||||
|
|
||||||
|
if ( empty( $api_key ) ) {
|
||||||
|
return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$prompt = self::build_generation_prompt( $peptide_name );
|
||||||
|
// Allow filtering of generation prompt before sending.
|
||||||
|
$prompt = apply_filters( 'psa_generation_prompt', $prompt, $peptide_name );
|
||||||
|
$response = self::call_openrouter(
|
||||||
|
$api_key,
|
||||||
|
$model ?: 'google/gemini-2.5-flash',
|
||||||
|
$prompt,
|
||||||
|
PSA_Config::GENERATION_MAX_TOKENS
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::parse_ai_response( $response );
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin settings with defaults.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function get_settings() {
|
||||||
|
$defaults = array(
|
||||||
|
'api_key' => '',
|
||||||
|
'ai_model' => '',
|
||||||
|
'validation_model' => '',
|
||||||
|
'auto_publish' => 'draft',
|
||||||
|
'use_pubchem' => '1',
|
||||||
|
);
|
||||||
|
|
||||||
|
$settings = wp_parse_args( get_option( 'psa_settings', array() ), $defaults );
|
||||||
|
|
||||||
|
// Prefer wp-config.php constant over database value for security.
|
||||||
|
if ( defined( 'PSA_OPENROUTER_KEY' ) && PSA_OPENROUTER_KEY ) {
|
||||||
|
$settings['api_key'] = PSA_OPENROUTER_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow filtering of settings before use.
|
||||||
|
$settings = apply_filters( 'psa_ai_settings', $settings );
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save meta fields for a peptide post.
|
||||||
|
*
|
||||||
|
* @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( $post_id, $ai_data, $pubchem_data ) {
|
||||||
|
$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'] ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow filtering of meta before save.
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the lightweight validation prompt.
|
||||||
|
*
|
||||||
|
* Uses wp_json_encode() to safely encode the peptide name within the prompt.
|
||||||
|
* This ensures special characters and quotes are properly escaped before sending to the AI.
|
||||||
|
* The JSON-encoded format also helps the AI parser recognize the input clearly.
|
||||||
*
|
*
|
||||||
* @param string $name The peptide name to validate.
|
* @param string $name The peptide name to validate.
|
||||||
* @return false|string False on valid, error message on fail.
|
* @return string Prompt text ready to send to OpenRouter API.
|
||||||
*/
|
*/
|
||||||
private static function validate_peptide_name( $name ) {
|
private static function build_validation_prompt( $name ) {
|
||||||
$name = trim( $name );
|
$json_name = wp_json_encode( $name );
|
||||||
if ( strlen( $name ) < PSA_Config::MIN_NAME_LENGTH ) {
|
return <<<PROMPT
|
||||||
return 'Peptide name must be at least ' . PSA_Config::MIN_NAME_LENGTH . ' characters.';
|
You are a peptide and biochemistry expert. Determine if the peptide name given in the following JSON-encoded string is a real, recognized peptide, protein fragment, or peptide-based compound.
|
||||||
|
|
||||||
|
Peptide name: {$json_name}
|
||||||
|
|
||||||
|
Return ONLY valid JSON (no markdown fences) with exactly these fields:
|
||||||
|
|
||||||
|
{
|
||||||
|
"is_valid": true or false,
|
||||||
|
"canonical_name": "The standard/official name for this peptide (correcting capitalization or common abbreviations)",
|
||||||
|
"reason": "Brief explanation of why this is or isn't a recognized peptide"
|
||||||
}
|
}
|
||||||
if ( strpos( $name, ',' ) !== false ) {
|
|
||||||
$parts = array_filter( explode( ',', $name ) );
|
Rules:
|
||||||
if ( count( $parts ) > PSA_Config::MAX_CITN / * comma-separated sequences */ ) {
|
- Return is_valid: true only for real peptides, peptide hormones, peptide-based drugs, neuropeptides, antimicrobial peptides, synthetic peptides used in research, or well-known protein fragments.
|
||||||
return 'Maximum ' . PSA_Config::MAX_CITRN . ' citrine residues in a single peptide.';
|
- Return is_valid: false for random words, made-up names, non-peptide drugs, non-peptide supplements, general proteins (unless commonly referred to as peptides), or anything that is clearly not a peptide.
|
||||||
}
|
- Be generous with research peptides and experimental compounds that are known in the scientific literature.
|
||||||
}
|
- Common examples that ARE valid: BPC-157, Thymosin Beta-4, TB-500, GHK-Cu, Melanotan II, Ipamorelin, CJC-1295, Semaglutide, Tirzepatide, AOD-9604, PT-141, DSIP, Epitalon, LL-37, Oxytocin, Vasopressin, Substance P, etc.
|
||||||
return false;
|
PROMPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full AI generation process.
|
* Build the comprehensive generation prompt.
|
||||||
*
|
*
|
||||||
* @param int $post_id The peptide post ID.
|
* Constructs a detailed prompt that instructs the AI to generate structured JSON output
|
||||||
* @param string $peptide_name The peptide name.
|
* with all required fields for a complete peptide database entry (name, sequence, mechanism,
|
||||||
|
* research benefits, dosing, safety, legal status, references, etc).
|
||||||
|
*
|
||||||
|
* The prompt is designed to ensure the AI returns clean, factual information suitable for
|
||||||
|
* scientific audiences. Uses JSON encoding for the peptide name to properly escape special characters.
|
||||||
|
*
|
||||||
|
* @param string $peptide_name The peptide to research.
|
||||||
|
* @return string Prompt text ready to send to OpenRouter API.
|
||||||
*/
|
*/
|
||||||
private static function generate_peptide_content( $post_id, $peptide_name ) {
|
private static function build_generation_prompt( $peptide_name ) {
|
||||||
$api_key = get_option( 'cst_custom_post_options_psa_api_key' );
|
$json_name = wp_json_encode( $peptide_name );
|
||||||
$model = get_option( 'cst_custom_post_options_psa_model' );;
|
return <<<PROMPT
|
||||||
if ( ! $api_key || ! $model ) {
|
You are a scientific writer specializing in peptide research. Generate a comprehensive database entry for the peptide given in the following JSON-encoded string.
|
||||||
error_log( 'PSA: Missing openirouter configuration (API key/model)' );
|
|
||||||
return, return (new WP_Error( 'missing_config', 'Missing API key or model.' ));
|
Peptide name: {$json_name}
|
||||||
|
|
||||||
|
Return ONLY valid JSON (no markdown fences, no commentary) with exactly these fields:
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Official peptide name",
|
||||||
|
"aliases": "Comma-separated list of alternative names, abbreviations, or synonyms",
|
||||||
|
"category_label": "e.g. Tissue Healing / Cytoprotective, or GH Secretagogue, etc.",
|
||||||
|
"origin": "Origin description, e.g. Synthetic (derived from human gastric juice protein)",
|
||||||
|
"sequence": "Full amino acid sequence in three-letter code with dashes, e.g. Gly-Glu-Pro-Pro-Pro (N amino acids). Use single-letter if three-letter not standard.",
|
||||||
|
"molecular_weight": "Molecular weight in Daltons (e.g. 1419.53 Da)",
|
||||||
|
"molecular_formula": "Chemical formula (e.g. C62H98N16O22)",
|
||||||
|
"overview": "2-3 paragraphs: what this peptide is, its origin, discovery, and significance. Written for a scientifically literate audience.",
|
||||||
|
"mechanism": "Detailed mechanism of action. Start with an intro paragraph, then describe each pathway as a labeled item like: Pathway name: Description. Separate items with double newlines.",
|
||||||
|
"research_benefits": "Detailed research applications organized by therapeutic area. Use area names as subheadings followed by a paragraph, e.g.:\n\nMusculoskeletal Healing\nParagraph about musculoskeletal research...\n\nGastrointestinal Protection\nParagraph about GI research...\n\nInclude 3-5 areas depending on the peptide.",
|
||||||
|
"administration_dosing": "Include a disclaimer that this is from research only. Then describe: typical research dose range, routes of administration, frequency, and duration from studies.",
|
||||||
|
"safety_side_effects": "Known safety profile from animal studies. Include any anecdotally reported side effects. Note theoretical concerns. Mention that long-term human safety data is limited.",
|
||||||
|
"legal_regulatory": "Current regulatory and legal status. Mention FDA status, WADA status if applicable, classification as research chemical.",
|
||||||
|
"references": "5-8 real published scientific references with authors, title, journal, year. Use real studies only.",
|
||||||
|
"category": "Classify into exactly ONE: GH Secretagogues, Healing & Repair, Melanocortin Peptides, Metabolic & Anti-Aging, Nootropic & Neuroprotective"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose prompt with peptide name info.
|
Important:
|
||||||
$prompt = self::prompt_peptide( $peptide_name );
|
- Use factual, evidence-based information only.
|
||||||
|
- If information is uncertain or unknown, say so explicitly rather than fabricating data.
|
||||||
$response = self::call_openrouter( $api_key, $model, $prompt );
|
- All content is for research and educational purposes.
|
||||||
if ( is_wp_error( $response ) ) {
|
- Write in clean paragraphs. Do not use markdown formatting symbols like # or ** or *.
|
||||||
error_log( 'PSA: AI call failed: ' . $response->get_error_message() );
|
- For research_benefits, use the therapeutic area name on its own line as a subheading, followed by a descriptive paragraph.
|
||||||
return, return $response;
|
PROMPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ai_data = self::parse_ai_response( $response );
|
// -------------------------------------------------------------------------
|
||||||
if ( is_wp_error( $ai_data ) ) {
|
// OpenRouter API Call
|
||||||
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.
|
* Call OpenRouter API with automatic retry on rate limits.
|
||||||
* @param string $peptide_name The peptide name (may be a sequence).
|
*
|
||||||
* @return string Formatted prompt.
|
* Implements exponential backoff retry strategy for rate limit (429) errors.
|
||||||
*/
|
* On first rate limit: waits 5s before retry.
|
||||||
private static function prompt_peptide( $peptide_name ) {
|
* On second rate limit: waits 10s before retry.
|
||||||
$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:
|
* On third rate limit: waits 20s before final attempt.
|
||||||
/
|
* Other errors are returned immediately without retry.
|
||||||
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 $api_key The OpenRouter API key.
|
||||||
* @param string $model The model identifier.
|
* @param string $model The model identifier.
|
||||||
* @param string $promt The prompt text.
|
* @param string $prompt The prompt text.
|
||||||
* @param int $max_tokens Maximum tokens to generate.
|
* @param int $max_tokens Maximum tokens to generate.
|
||||||
* @return string|WP_Error Response text or error.
|
* @return string|WP_Error Response text or error.
|
||||||
*/
|
*/
|
||||||
@@ -165,7 +393,7 @@ class PSA_AI_Generator {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limited â retry with exponential backoff.
|
// Rate limited — retry with exponential backoff.
|
||||||
if ( $attempt < $max_retries ) {
|
if ( $attempt < $max_retries ) {
|
||||||
$delay = $base_delay * pow( 2, $attempt - 1 ); // 5s, 10s, 20s
|
$delay = $base_delay * pow( 2, $attempt - 1 ); // 5s, 10s, 20s
|
||||||
error_log( 'PSA: Rate limited on attempt ' . $attempt . '/' . $max_retries . '. Retrying in ' . $delay . 's...' );
|
error_log( 'PSA: Rate limited on attempt ' . $attempt . '/' . $max_retries . '. Retrying in ' . $delay . 's...' );
|
||||||
@@ -194,7 +422,7 @@ class PSA_AI_Generator {
|
|||||||
self::API_URL,
|
self::API_URL,
|
||||||
array(
|
array(
|
||||||
'timeout' => 120,
|
'timeout' => 120,
|
||||||
headers' => array(
|
'headers' => array(
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'Authorization' => 'Bearer ' . $api_key,
|
'Authorization' => 'Bearer ' . $api_key,
|
||||||
'HTTP-Referer' => $site_url,
|
'HTTP-Referer' => $site_url,
|
||||||
@@ -232,10 +460,9 @@ class PSA_AI_Generator {
|
|||||||
}
|
}
|
||||||
if ( 402 === $code ) {
|
if ( 402 === $code ) {
|
||||||
error_log( 'PSA: OpenRouter insufficient credits (402)' );
|
error_log( 'PSA: OpenRouter insufficient credits (402)' );
|
||||||
return new WP_Error( 'ainsufficient_credits', 'OpenRouter account has insufficient credits. Please add funds at openrouter.ai";
|
return new WP_Error( 'insufficient_credits', 'OpenRouter account has insufficient credits. Please add funds at openrouter.ai.' );
|
||||||
}
|
}
|
||||||
|
$msg = $body['error']['message'] ?? ( 'Unknown error (HTTP ' . $code . ')' );
|
||||||
$is = isset( $body['error'] ) ? $body['error']['message'] : ( 'Unknown error (HTTP ' . $code . ')' );
|
|
||||||
error_log( 'PSA: OpenRouter API error (' . $code . '): ' . $msg );
|
error_log( 'PSA: OpenRouter API error (' . $code . '): ' . $msg );
|
||||||
error_log( 'PSA: OpenRouter raw response: ' . substr( $raw_body, 0, 500 ) );
|
error_log( 'PSA: OpenRouter raw response: ' . substr( $raw_body, 0, 500 ) );
|
||||||
return new WP_Error( 'api_error', 'OpenRouter API error: ' . $msg );
|
return new WP_Error( 'api_error', 'OpenRouter API error: ' . $msg );
|
||||||
@@ -248,3 +475,218 @@ class PSA_AI_Generator {
|
|||||||
|
|
||||||
return $body['choices'][0]['message']['content'];
|
return $body['choices'][0]['message']['content'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse AI response text into structured data.
|
||||||
|
*
|
||||||
|
* @param string $response_text Raw response text.
|
||||||
|
* @return array|WP_Error Parsed data or error.
|
||||||
|
*/
|
||||||
|
private static function parse_ai_response( $response_text ) {
|
||||||
|
// Strip any <think>...</think> tags (some models like DeepSeek include reasoning).
|
||||||
|
$cleaned = preg_replace( '/<think>.*?<\/think>/s', '', $response_text );
|
||||||
|
|
||||||
|
// Strip markdown code fences if present.
|
||||||
|
$cleaned = preg_replace( '/^```(?:json)?\s*/m', '', $cleaned );
|
||||||
|
$cleaned = preg_replace( '/\s*```\s*$/m', '', $cleaned );
|
||||||
|
$cleaned = trim( $cleaned );
|
||||||
|
|
||||||
|
$data = json_decode( $cleaned, true );
|
||||||
|
if ( JSON_ERROR_NONE !== json_last_error() ) {
|
||||||
|
error_log( 'PSA: JSON parse error: ' . json_last_error_msg() . '. Raw (first 500 chars): ' . substr( $response_text, 0, 500 ) );
|
||||||
|
return new WP_Error(
|
||||||
|
'parse_error',
|
||||||
|
'Failed to parse AI response as JSON: ' . json_last_error_msg()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a matching Echo Knowledge Base article with full structured content.
|
||||||
|
*
|
||||||
|
* @param int $peptide_post_id The peptide post ID.
|
||||||
|
* @param array $ai_data The parsed AI data.
|
||||||
|
* @param string $peptide_name The peptide name.
|
||||||
|
*/
|
||||||
|
private static function create_kb_article( $peptide_post_id, $ai_data, $peptide_name ) {
|
||||||
|
if ( ! post_type_exists( 'epkb_post_type_1' ) ) {
|
||||||
|
error_log( 'PSA: Echo Knowledge Base not active — skipping KB article.' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = sanitize_text_field( $ai_data['name'] ?? $peptide_name );
|
||||||
|
$existing = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => 'epkb_post_type_1',
|
||||||
|
'title' => $title,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'numberposts' => 1,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! empty( $existing ) ) {
|
||||||
|
error_log( 'PSA: KB article already exists for "' . $peptide_name . '".' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build header meta line.
|
||||||
|
$cat_label = ! empty( $ai_data['category_label'] ) ? $ai_data['category_label'] : '';
|
||||||
|
$origin = ! empty( $ai_data['origin'] ) ? $ai_data['origin'] : '';
|
||||||
|
$sequence = ! empty( $ai_data['sequence'] ) ? $ai_data['sequence'] : '';
|
||||||
|
|
||||||
|
$header = '';
|
||||||
|
if ( $cat_label ) {
|
||||||
|
$header .= '<strong>Category:</strong> ' . esc_html( $cat_label );
|
||||||
|
}
|
||||||
|
if ( $origin ) {
|
||||||
|
$header .= ' | <strong>Origin:</strong> ' . esc_html( $origin );
|
||||||
|
}
|
||||||
|
if ( $sequence ) {
|
||||||
|
$header .= ' | <strong>Sequence:</strong> ' . esc_html( $sequence );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build full article content.
|
||||||
|
$c = $header ? '<p>' . $header . '</p>' . "\n\n" : '';
|
||||||
|
|
||||||
|
// Overview.
|
||||||
|
$c .= '<h2>Overview</h2>' . "\n";
|
||||||
|
$c .= self::paragraphs( $ai_data['overview'] ?? $ai_data['description'] ?? '' );
|
||||||
|
|
||||||
|
// Mechanism of Action.
|
||||||
|
if ( ! empty( $ai_data['mechanism'] ) ) {
|
||||||
|
$c .= '<h2>Mechanism of Action</h2>' . "\n";
|
||||||
|
$c .= self::paragraphs( $ai_data['mechanism'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Research & Potential Benefits (with h3 subheadings).
|
||||||
|
if ( ! empty( $ai_data['research_benefits'] ) || ! empty( $ai_data['research_applications'] ) ) {
|
||||||
|
$c .= '<h2>Research & Potential Benefits</h2>' . "\n";
|
||||||
|
$c .= '<p><em>The following potential benefits are based on animal studies and preclinical research. Human clinical trial data is extremely limited.</em></p>' . "\n";
|
||||||
|
$research = $ai_data['research_benefits'] ?? $ai_data['research_applications'] ?? '';
|
||||||
|
$c .= self::research_with_subheadings( $research );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Administration & Dosing.
|
||||||
|
if ( ! empty( $ai_data['administration_dosing'] ) || ! empty( $ai_data['dosage_info'] ) ) {
|
||||||
|
$c .= '<h2>Administration & Dosing (Research Context)</h2>' . "\n";
|
||||||
|
$c .= '<p><strong>Important:</strong> The following dosing information is derived from animal studies and extrapolated research. This peptide is not approved for human use. This information is provided for educational purposes only.</p>' . "\n";
|
||||||
|
$c .= self::paragraphs( $ai_data['administration_dosing'] ?? $ai_data['dosage_info'] ?? '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety & Side Effects.
|
||||||
|
if ( ! empty( $ai_data['safety_side_effects'] ) || ! empty( $ai_data['safety_profile'] ) ) {
|
||||||
|
$c .= '<h2>Safety & Side Effects</h2>' . "\n";
|
||||||
|
$c .= self::paragraphs( $ai_data['safety_side_effects'] ?? $ai_data['safety_profile'] ?? '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legal & Regulatory Status.
|
||||||
|
if ( ! empty( $ai_data['legal_regulatory'] ) ) {
|
||||||
|
$c .= '<h2>Legal & Regulatory Status</h2>' . "\n";
|
||||||
|
$c .= self::paragraphs( $ai_data['legal_regulatory'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// References.
|
||||||
|
if ( ! empty( $ai_data['references'] ) ) {
|
||||||
|
$c .= '<h2>Notable Research References</h2>' . "\n";
|
||||||
|
$c .= self::paragraphs( $ai_data['references'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Medical Disclaimer.
|
||||||
|
$c .= "\n" . '<p><strong>Medical Disclaimer:</strong> The information on this page is provided for educational and research purposes only. This peptide is not approved for human therapeutic use. Nothing on this page constitutes medical advice. Always consult a licensed healthcare professional before using any peptide, supplement, or research chemical.</p>';
|
||||||
|
|
||||||
|
$kb_post_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_title' => $title,
|
||||||
|
'post_content' => wp_kses_post( $c ),
|
||||||
|
'post_type' => 'epkb_post_type_1',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $kb_post_id ) ) {
|
||||||
|
error_log( 'PSA: Failed to create KB article: ' . $kb_post_id->get_error_message() );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign KB category.
|
||||||
|
$category_map = array(
|
||||||
|
'GH Secretagogues' => 'gh-secretagogues',
|
||||||
|
'Healing & Repair' => 'healing-repair',
|
||||||
|
'Melanocortin Peptides' => 'melanocortin-peptides',
|
||||||
|
'Metabolic & Anti-Aging' => 'metabolic-anti-aging',
|
||||||
|
'Nootropic & Neuroprotective' => 'nootropic-neuroprotective',
|
||||||
|
);
|
||||||
|
|
||||||
|
$cat_name = trim( $ai_data['category'] ?? '' );
|
||||||
|
if ( isset( $category_map[ $cat_name ] ) ) {
|
||||||
|
$term = get_term_by( 'slug', $category_map[ $cat_name ], 'epkb_post_type_1_category' );
|
||||||
|
if ( $term ) {
|
||||||
|
wp_set_object_terms( $kb_post_id, array( (int) $term->term_id ), 'epkb_post_type_1_category' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta( $peptide_post_id, 'psa_kb_article_id', $kb_post_id );
|
||||||
|
update_post_meta( $kb_post_id, 'psa_peptide_post_id', $peptide_post_id );
|
||||||
|
|
||||||
|
error_log( 'PSA: Created KB article #' . $kb_post_id . ' for "' . $peptide_name . '"' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert text to HTML paragraphs.
|
||||||
|
*/
|
||||||
|
private static function paragraphs( $text ) {
|
||||||
|
if ( empty( $text ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$text = str_replace( "\r\n", "\n", $text );
|
||||||
|
$parts = preg_split( '/\n{2,}/', trim( $text ) );
|
||||||
|
$out = '';
|
||||||
|
foreach ( $parts as $p ) {
|
||||||
|
$p = trim( $p );
|
||||||
|
if ( '' === $p ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out .= '<p>' . wp_kses_post( $p ) . '</p>' . "\n";
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse research text into h3 subheadings + paragraphs.
|
||||||
|
*/
|
||||||
|
private static function research_with_subheadings( $text ) {
|
||||||
|
if ( empty( $text ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$text = str_replace( "\r\n", "\n", trim( $text ) );
|
||||||
|
$blocks = preg_split( '/\n{2,}/', $text );
|
||||||
|
$out = '';
|
||||||
|
foreach ( $blocks as $block ) {
|
||||||
|
$block = trim( $block );
|
||||||
|
if ( '' === $block ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If block is short (under 80 chars) and has no period, treat as a subheading.
|
||||||
|
if ( strlen( $block ) < 80 && strpos( $block, '.' ) === false ) {
|
||||||
|
$out .= '<h3>' . esc_html( $block ) . '</h3>' . "\n";
|
||||||
|
} else {
|
||||||
|
// Check if first line is a subheading followed by content.
|
||||||
|
$first_nl = strpos( $block, "\n" );
|
||||||
|
if ( $first_nl !== false ) {
|
||||||
|
$first_line = trim( substr( $block, 0, $first_nl ) );
|
||||||
|
$rest = trim( substr( $block, $first_nl + 1 ) );
|
||||||
|
if ( strlen( $first_line ) < 80 && strpos( $first_line, '.' ) === false ) {
|
||||||
|
$out .= '<h3>' . esc_html( $first_line ) . '</h3>' . "\n";
|
||||||
|
$out .= '<p>' . wp_kses_post( $rest ) . '</p>' . "\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$out .= '<p>' . wp_kses_post( $block ) . '</p>' . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user