From 76235ef4d7969d88b8d989d97f4f21815b566d7e Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Mon, 20 Apr 2026 02:19:52 +0700 Subject: [PATCH] v0.6.1: peptide auto-linking + extract Content_Prompts class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When any peptide from the PR Core database is mentioned in a generated article, the first mention is now automatically linked to its /peptides/ page. Gracefully degrades if PR Core is not active. Extracted all prompt construction into Content_Prompts (252 lines), slimming Content_Generator from 324 → 162 lines. Both under 300. Co-Authored-By: Claude Opus 4.6 --- ARCHITECTURE.md | 1 + CHANGELOG.md | 18 ++ includes/core/class-content-generator.php | 163 ++------------ includes/core/class-content-prompts.php | 252 ++++++++++++++++++++++ prautoblogger.php | 4 +- 5 files changed, 294 insertions(+), 144 deletions(-) create mode 100644 includes/core/class-content-prompts.php diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 38cc3d1..e843624 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -133,6 +133,7 @@ prautoblogger/ │ │ ├── class-idea-scorer.php # Ranks and deduplicates article ideas (delegates to Semantic_Dedup) │ │ ├── class-semantic-dedup.php # Embedding-based cosine similarity dedup with keyword fallback │ │ ├── class-content-generator.php# Writer agent — manages the generation pipeline +│ │ ├── class-content-prompts.php # Builds all LLM prompts for content generation (system, stage, linking rules) │ │ ├── class-chief-editor.php # Editor agent — LLM-powered editorial review │ │ ├── class-publisher.php # Creates WordPress posts from approved content │ │ ├── class-post-assembler.php # Post-creation helpers: taxonomy, log linking, images, sanitization diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f11be6..a10d14f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to PRAutoBlogger will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project uses [Semantic Versioning](https://semver.org/). +## [0.6.1] — 2026-04-20 + +### Added +- **Peptide auto-linking.** When any peptide name from the PR Core database is + mentioned in a generated article, the first mention is automatically linked to + the corresponding `/peptides/{slug}/` page. The system prompt now includes a + complete peptide database reference list alongside the existing article links. + Gracefully degrades if PR Core is not active (`post_type_exists()` guard). + +### Changed +- **Extracted prompt builders into Content_Prompts class.** Moved all prompt + construction (system prompt, stage prompts, linking rules) from + `Content_Generator` into a new `Content_Prompts` static helper. Generator + dropped from 324 → 162 lines; prompts class is 252 lines. Both under 300. +- **Internal link rules strengthened.** Linking rules section now explicitly + instructs the model to link every peptide's first mention and never fabricate + URLs, with real published article and peptide page URLs provided. + ## [0.6.0] — 2026-04-20 ### Added diff --git a/includes/core/class-content-generator.php b/includes/core/class-content-generator.php index 62cab6c..b2b4164 100755 --- a/includes/core/class-content-generator.php +++ b/includes/core/class-content-generator.php @@ -9,11 +9,12 @@ declare(strict_types=1); * - multi_step: Outline → Draft → Polish pipeline (higher quality, more cost). * * Triggered by: PRAutoBlogger::run_generation_pipeline() (step 4). - * Dependencies: LLM_Provider_Interface, Cost_Tracker. + * Dependencies: LLM_Provider_Interface, Cost_Tracker, Content_Prompts. * - * @see core/class-chief-editor.php — Reviews output of this class. + * @see core/class-content-prompts.php — Builds all LLM prompts used here. + * @see core/class-chief-editor.php — Reviews output of this class. * @see providers/interface-llm-provider.php — LLM used for generation. - * @see ARCHITECTURE.md — Data flow step 4. + * @see ARCHITECTURE.md — Data flow step 4. */ class PRAutoBlogger_Content_Generator { @@ -66,31 +67,20 @@ class PRAutoBlogger_Content_Generator { * @return string Generated HTML content. */ private function generate_single_pass( PRAutoBlogger_Content_Request $request ): string { - $model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL ); - $idea = $request->get_idea(); - - $system = $this->build_writer_system_prompt( $request ); - $user = $this->build_single_pass_prompt( $request ); + $model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL ); $response = $this->llm->send_chat_completion( [ - [ 'role' => 'system', 'content' => $system ], - [ 'role' => 'user', 'content' => $user ], + [ 'role' => 'system', 'content' => PRAutoBlogger_Content_Prompts::build_system( $request ) ], + [ 'role' => 'user', 'content' => PRAutoBlogger_Content_Prompts::build_single_pass( $request ) ], ], $model, - [ - 'temperature' => 0.7, - 'max_tokens' => 4000, - ] + [ 'temperature' => 0.7, 'max_tokens' => 4000 ] ); $this->cost_tracker->log_api_call( - null, - 'draft', - $this->llm->get_provider_name(), - $response['model'], - $response['prompt_tokens'], - $response['completion_tokens'] + null, 'draft', $this->llm->get_provider_name(), + $response['model'], $response['prompt_tokens'], $response['completion_tokens'] ); return $response['content']; @@ -100,13 +90,8 @@ class PRAutoBlogger_Content_Generator { private function generate_multi_step( PRAutoBlogger_Content_Request $request ): string { $model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL ); - // Step 1: Generate outline. - $outline = $this->stage_outline( $request, $model ); - - // Step 2: Generate draft from outline. - $draft = $this->stage_draft( $request, $outline, $model ); - - // Step 3: Polish the draft. + $outline = $this->stage_outline( $request, $model ); + $draft = $this->stage_draft( $request, $outline, $model ); $polished = $this->stage_polish( $request, $draft, $model ); return $polished; @@ -114,29 +99,12 @@ class PRAutoBlogger_Content_Generator { /** Multi-step stage 1: Generate an article outline. */ private function stage_outline( PRAutoBlogger_Content_Request $request, string $model ): string { - $idea = $request->get_idea(); - - $prompt = sprintf( - "Create a detailed outline for a blog post titled: \"%s\"\n\n" . - "Topic: %s\nArticle type: %s\n\nKey points to cover:\n%s\n\n" . - "Target keywords: %s\n\n" . - "The outline should have 4-6 main sections with bullet points under each. " . - "Include an introduction hook and a conclusion with a call to action. " . - "Word count target: %d-%d words.\n\n" . - "Plan the structure to satisfy EVERY requirement in your system prompt style guide.", - $idea->get_suggested_title(), - $idea->get_topic(), - $idea->get_article_type(), - implode( "\n- ", $idea->get_key_points() ), - implode( ', ', $idea->get_target_keywords() ), - $request->get_min_word_count(), - $request->get_max_word_count() - ); + $system = PRAutoBlogger_Content_Prompts::build_system( $request ); $response = $this->llm->send_chat_completion( [ - [ 'role' => 'system', 'content' => $this->build_writer_system_prompt( $request ) ], - [ 'role' => 'user', 'content' => $prompt ], + [ 'role' => 'system', 'content' => $system ], + [ 'role' => 'user', 'content' => PRAutoBlogger_Content_Prompts::build_outline( $request ) ], ], $model, [ 'temperature' => 0.5, 'max_tokens' => 1500 ] @@ -152,29 +120,12 @@ class PRAutoBlogger_Content_Generator { /** Multi-step stage 2: Write the full draft from the outline. */ private function stage_draft( PRAutoBlogger_Content_Request $request, string $outline, string $model ): string { - $prompt = sprintf( - "Using this outline, write the full blog post in HTML format.\n\n" . - "OUTLINE:\n%s\n\n" . - "Requirements:\n" . - "- Write in a %s tone\n" . - "- Target %d-%d words\n" . - "- Use proper HTML headings (h2, h3), paragraphs, and lists\n" . - "- Include an engaging introduction and strong conclusion\n" . - "- Naturally incorporate these keywords: %s\n" . - "- Do NOT include the title in the HTML (it will be set separately)\n" . - "- Do NOT wrap in , , or tags — just the article content\n" . - "- Follow EVERY formatting and structural requirement from your system prompt style guide", - $outline, - $request->get_tone(), - $request->get_min_word_count(), - $request->get_max_word_count(), - implode( ', ', $request->get_idea()->get_target_keywords() ) - ); + $system = PRAutoBlogger_Content_Prompts::build_system( $request ); $response = $this->llm->send_chat_completion( [ - [ 'role' => 'system', 'content' => $this->build_writer_system_prompt( $request ) ], - [ 'role' => 'user', 'content' => $prompt ], + [ 'role' => 'system', 'content' => $system ], + [ 'role' => 'user', 'content' => PRAutoBlogger_Content_Prompts::build_draft( $request, $outline ) ], ], $model, [ 'temperature' => 0.7, 'max_tokens' => 4000 ] @@ -190,23 +141,12 @@ class PRAutoBlogger_Content_Generator { /** Multi-step stage 3: Polish and refine the draft. */ private function stage_polish( PRAutoBlogger_Content_Request $request, string $draft, string $model ): string { - $prompt = "Review and polish this blog post draft. Improve:\n" . - "1. Flow and readability\n" . - "2. SEO optimization (headings, keyword placement)\n" . - "3. Engagement (hooks, transitions, call-to-action)\n" . - "4. Accuracy and clarity\n" . - "5. Remove any filler or redundant sentences\n\n" . - "IMPORTANT: Preserve all bullet points, numbered lists, hyperlinks, and " . - "structural elements from the draft. Do NOT flatten lists into prose or " . - "remove links. Ensure every requirement from your system prompt style " . - "guide is satisfied in the final output.\n\n" . - "Return the polished HTML content only. Do not add commentary.\n\n" . - "DRAFT:\n" . $draft; + $system = PRAutoBlogger_Content_Prompts::build_system( $request ); $response = $this->llm->send_chat_completion( [ - [ 'role' => 'system', 'content' => $this->build_writer_system_prompt( $request ) ], - [ 'role' => 'user', 'content' => $prompt ], + [ 'role' => 'system', 'content' => $system ], + [ 'role' => 'user', 'content' => PRAutoBlogger_Content_Prompts::build_polish( $draft ) ], ], $model, [ 'temperature' => 0.4, 'max_tokens' => 4000 ] @@ -219,65 +159,4 @@ class PRAutoBlogger_Content_Generator { return $response['content']; } - - /** - * Build the system prompt shared across all writing stages. - * - * @param PRAutoBlogger_Content_Request $request Content request with settings. - * @return string - */ - private function build_writer_system_prompt( PRAutoBlogger_Content_Request $request ): string { - $niche = $request->get_niche_description(); - $prompt = "You are an expert blog writer"; - if ( '' !== $niche ) { - $prompt .= " specializing in {$niche}"; - } - $prompt .= ". Write well-researched, engaging, SEO-friendly content. "; - $prompt .= "Use a {$request->get_tone()} tone. "; - $prompt .= "Output HTML content only — no markdown, no code fences, no commentary."; - - // Append user-defined writing instructions as a mandatory style guide. - // Framed prominently to ensure the model treats them as requirements, - // not optional suggestions — "Additional instructions" was too weak. - $instructions = trim( $request->get_writing_instructions() ); - if ( '' !== $instructions ) { - $prompt .= "\n\n--- MANDATORY STYLE GUIDE ---\n"; - $prompt .= "You MUST follow every requirement below. These override any conflicting defaults:\n\n"; - $prompt .= $instructions; - $prompt .= "\n--- END STYLE GUIDE ---"; - } - - return $prompt; - } - - /** - * Build the user prompt for single-pass generation. - * - * @param PRAutoBlogger_Content_Request $request - * - * @return string - */ - private function build_single_pass_prompt( PRAutoBlogger_Content_Request $request ): string { - $idea = $request->get_idea(); - - return sprintf( - "Write a complete blog post in HTML format.\n\n" . - "Title: %s\nTopic: %s\nType: %s\n\nKey points:\n- %s\n\n" . - "Keywords: %s\n\n" . - "Requirements:\n" . - "- %d-%d words\n" . - "- Proper HTML (h2, h3, p, ul/li)\n" . - "- Engaging intro, strong conclusion with CTA\n" . - "- Do NOT include the title or / tags\n" . - "- Output HTML only, no markdown or commentary\n" . - "- Follow EVERY formatting and structural requirement from your system prompt style guide", - $idea->get_suggested_title(), - $idea->get_topic(), - $idea->get_article_type(), - implode( "\n- ", $idea->get_key_points() ), - implode( ', ', $idea->get_target_keywords() ), - $request->get_min_word_count(), - $request->get_max_word_count() - ); - } } diff --git a/includes/core/class-content-prompts.php b/includes/core/class-content-prompts.php new file mode 100644 index 0000000..096699d --- /dev/null +++ b/includes/core/class-content-prompts.php @@ -0,0 +1,252 @@ +get_niche_description(); + $prompt = 'You are an expert blog writer'; + if ( '' !== $niche ) { + $prompt .= " specializing in {$niche}"; + } + $prompt .= '. Write well-researched, engaging, SEO-friendly content. '; + $prompt .= "Use a {$request->get_tone()} tone. "; + $prompt .= 'Output HTML content only — no markdown, no code fences, no commentary.'; + + // Append user-defined writing instructions as a mandatory style guide. + $instructions = trim( $request->get_writing_instructions() ); + if ( '' !== $instructions ) { + $prompt .= "\n\n--- MANDATORY STYLE GUIDE ---\n"; + $prompt .= "You MUST follow every requirement below. These override any conflicting defaults:\n\n"; + $prompt .= $instructions; + $prompt .= "\n--- END STYLE GUIDE ---"; + } + + $prompt .= "\n\n" . self::build_linking_rules(); + + return $prompt; + } + + /** + * Build the user prompt for single-pass generation. + * + * @param PRAutoBlogger_Content_Request $request Content request with settings. + * @return string User prompt. + */ + public static function build_single_pass( PRAutoBlogger_Content_Request $request ): string { + $idea = $request->get_idea(); + + return sprintf( + "Write a complete blog post in HTML format.\n\n" . + "Title: %s\nTopic: %s\nType: %s\n\nKey points:\n- %s\n\n" . + "Keywords: %s\n\n" . + "Requirements:\n" . + "- %d-%d words\n" . + "- Proper HTML (h2, h3, p, ul/li)\n" . + "- Engaging intro, strong conclusion with CTA\n" . + "- Do NOT include the title or / tags\n" . + "- Output HTML only, no markdown or commentary\n" . + "- Follow EVERY formatting and structural requirement from your system prompt style guide", + $idea->get_suggested_title(), + $idea->get_topic(), + $idea->get_article_type(), + implode( "\n- ", $idea->get_key_points() ), + implode( ', ', $idea->get_target_keywords() ), + $request->get_min_word_count(), + $request->get_max_word_count() + ); + } + + /** + * Build the outline stage prompt (multi-step stage 1). + * + * @param PRAutoBlogger_Content_Request $request Content request with settings. + * @return string Outline prompt. + */ + public static function build_outline( PRAutoBlogger_Content_Request $request ): string { + $idea = $request->get_idea(); + + return sprintf( + "Create a detailed outline for a blog post titled: \"%s\"\n\n" . + "Topic: %s\nArticle type: %s\n\nKey points to cover:\n%s\n\n" . + "Target keywords: %s\n\n" . + "The outline should have 4-6 main sections with bullet points under each. " . + "Include an introduction hook and a conclusion with a call to action. " . + "Word count target: %d-%d words.\n\n" . + "Plan the structure to satisfy EVERY requirement in your system prompt style guide.", + $idea->get_suggested_title(), + $idea->get_topic(), + $idea->get_article_type(), + implode( "\n- ", $idea->get_key_points() ), + implode( ', ', $idea->get_target_keywords() ), + $request->get_min_word_count(), + $request->get_max_word_count() + ); + } + + /** + * Build the draft stage prompt (multi-step stage 2). + * + * @param PRAutoBlogger_Content_Request $request Content request with settings. + * @param string $outline The outline from stage 1. + * @return string Draft prompt. + */ + public static function build_draft( PRAutoBlogger_Content_Request $request, string $outline ): string { + return sprintf( + "Using this outline, write the full blog post in HTML format.\n\n" . + "OUTLINE:\n%s\n\n" . + "Requirements:\n" . + "- Write in a %s tone\n" . + "- Target %d-%d words\n" . + "- Use proper HTML headings (h2, h3), paragraphs, and lists\n" . + "- Include an engaging introduction and strong conclusion\n" . + "- Naturally incorporate these keywords: %s\n" . + "- Do NOT include the title in the HTML (it will be set separately)\n" . + "- Do NOT wrap in , , or tags — just the article content\n" . + "- Follow EVERY formatting and structural requirement from your system prompt style guide", + $outline, + $request->get_tone(), + $request->get_min_word_count(), + $request->get_max_word_count(), + implode( ', ', $request->get_idea()->get_target_keywords() ) + ); + } + + /** + * Build the polish stage prompt (multi-step stage 3). + * + * @param string $draft The draft from stage 2. + * @return string Polish prompt. + */ + public static function build_polish( string $draft ): string { + return "Review and polish this blog post draft. Improve:\n" . + "1. Flow and readability\n" . + "2. SEO optimization (headings, keyword placement)\n" . + "3. Engagement (hooks, transitions, call-to-action)\n" . + "4. Accuracy and clarity\n" . + "5. Remove any filler or redundant sentences\n\n" . + "IMPORTANT: Preserve all bullet points, numbered lists, hyperlinks, and " . + "structural elements from the draft. Do NOT flatten lists into prose or " . + "remove links. Ensure every requirement from your system prompt style " . + "guide is satisfied in the final output.\n\n" . + "Return the polished HTML content only. Do not add commentary.\n\n" . + "DRAFT:\n" . $draft; + } + + /** + * Build the linking rules section (internal articles + peptide database). + * + * Fetches published blog posts and peptide pages, formats them as a + * reference list so the model uses real URLs instead of fabricating them. + * + * @return string Complete linking rules block. + */ + private static function build_linking_rules(): string { + $rules = "--- LINKING RULES ---\n"; + $rules .= "NEVER fabricate or invent URLs. You do NOT have access to external sources.\n"; + $rules .= "Only use the internal links listed below when linking within the article.\n"; + $rules .= "If no listed page is relevant to a section, do not insert a link.\n"; + $rules .= "IMPORTANT: Whenever ANY peptide name from the peptide database list below\n"; + $rules .= "is mentioned in the article, ALWAYS link the FIRST mention to its database page.\n\n"; + + $rules .= self::build_article_links(); + $rules .= "\n"; + $rules .= self::build_peptide_links(); + + $rules .= "--- END LINKING RULES ---"; + + return $rules; + } + + /** + * Build a reference list of published blog posts for internal linking. + * + * @return string Formatted list, or a fallback note. + */ + private static function build_article_links(): string { + $posts = get_posts( [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'posts_per_page' => 30, + 'orderby' => 'date', + 'order' => 'DESC', + 'fields' => 'ids', + ] ); + + if ( empty( $posts ) ) { + return "No published articles available for internal linking yet.\n"; + } + + $lines = [ "Available article links (use where topically relevant):\n" ]; + foreach ( $posts as $post_id ) { + $title = get_the_title( $post_id ); + $url = get_permalink( $post_id ); + if ( $title && $url ) { + $lines[] = "- {$title}: {$url}"; + } + } + + return implode( "\n", $lines ) . "\n"; + } + + /** + * Build a reference list of all published peptide database pages. + * + * Every peptide mentioned in an article must link to its database page + * on first mention. This gives the model the real URLs to use. + * + * @return string Formatted peptide link list, or empty if CPT unavailable. + */ + private static function build_peptide_links(): string { + // PR Core may not be active — guard against missing post type. + if ( ! post_type_exists( 'pr_peptide' ) ) { + return ''; + } + + $peptides = get_posts( [ + 'post_type' => 'pr_peptide', + 'post_status' => 'publish', + 'posts_per_page' => 200, + 'orderby' => 'title', + 'order' => 'ASC', + 'fields' => 'ids', + ] ); + + if ( empty( $peptides ) ) { + return ''; + } + + $lines = [ "Peptide database pages — ALWAYS link the first mention of each peptide:\n" ]; + foreach ( $peptides as $peptide_id ) { + $title = get_the_title( $peptide_id ); + $url = get_permalink( $peptide_id ); + if ( $title && $url ) { + $lines[] = "- {$title}: {$url}"; + } + } + + return implode( "\n", $lines ) . "\n"; + } +} diff --git a/prautoblogger.php b/prautoblogger.php index 1899fa2..d66adee 100644 --- a/prautoblogger.php +++ b/prautoblogger.php @@ -8,7 +8,7 @@ declare(strict_types=1); * Plugin Name: PRAutoBlogger * Plugin URI: https://peptiderepo.com/prautoblogger * Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics. - * Version: 0.6.0 + * Version: 0.6.1 * Requires at least: 6.0 * Requires PHP: 7.4 * Author: PeptideRepo @@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) { | and limits without magic strings. */ -define( 'PRAUTOBLOGGER_VERSION', '0.6.0' ); +define( 'PRAUTOBLOGGER_VERSION', '0.6.1' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );