diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 05fc9b4..88d8fd7 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -16,9 +16,10 @@ PRAutoBlogger is a WordPress plugin that monitors social media (starting with Re │ ▼ ┌─────────────────────────┐ -│ 1. Source Collector │ Pulls raw posts/comments from social platforms -│ (Reddit RSS primary │ Reddit RSS/Atom feeds (primary) / .json (fallback) -│ / .json fallback) Stores raw data in `ab_source_data` table +│ 1. Source Collector │ Pulls data from enabled sources: +│ (Reddit + LLM Research)│ - Reddit RSS/Atom (primary) / .json (fallback) +│ │ - LLM Deep Research (reasoning model) +│ │ Stores raw data in `ab_source_data` table └────────────┬────────────┘ │ ▼ @@ -151,6 +152,7 @@ prautoblogger/ │ │ ├── class-open-router-pricing.php # Model pricing lookup and cost estimation │ │ ├── interface-source-provider.php # Contract for any social media source │ │ ├── class-reddit-json-client.php # Reddit HTTP client — RSS (primary) + .json (fallback) +│ │ ├── class-llm-research-provider.php # LLM deep research source (reasoning models) │ │ ├── class-reddit-provider.php # Reddit data collection orchestrator (RSS primary) │ │ ├── interface-image-provider.php # Contract for any image generation provider (incl. batch) │ │ ├── class-open-router-image-provider.php # OpenRouter image gen (single + batch dispatch) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c93f1d..27332f0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ 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.5.0] — 2026-04-20 + +### Added +- **LLM Deep Research source provider.** New data source that sends a configurable + research prompt to a reasoning-capable model (e.g. Grok 4.1 Fast, DeepSeek-R1) + to identify trending topics, emerging questions, misconceptions, and content gaps. + Findings feed into the content analyzer alongside Reddit data, giving the pipeline + both real-time community signals AND deep knowledge-base research. + - `includes/providers/class-llm-research-provider.php` — Source provider implementation. + - New admin settings in Sources tab: + - "LLM Deep Research" checkbox in Enabled Sources. + - "Research Model" model picker — pick a reasoning-capable model. + - "Research Prompt" textarea — customizable research brief with `{niche}` placeholder. + - Each research run produces 8-12 findings stored as `llm_research` source data. + - Deduplication via date-based source IDs (one fresh run per day, refreshes on re-run). + - Cost tracked as `llm_research` stage in generation logs. +- Content analyzer now formats LLM research findings with a cleaner label + (`[LLM Research] Relevance: N`) instead of the Reddit-specific `r/subreddit` format. + ## [0.4.9] — 2026-04-19 ### Added diff --git a/includes/admin/class-settings-fields-extended.php b/includes/admin/class-settings-fields-extended.php index b21dc84..26fdeeb 100644 --- a/includes/admin/class-settings-fields-extended.php +++ b/includes/admin/class-settings-fields-extended.php @@ -80,6 +80,26 @@ class PRAutoBlogger_Settings_Fields_Extended { */ public static function get_fields(): array { return [ + // ── Sources (extended) ────────────────────────────────────── + [ + 'id' => 'prautoblogger_research_model', + 'label' => __( 'Research Model', 'prautoblogger' ), + 'type' => 'model_select', + 'section' => 'prautoblogger_sources', + 'default' => PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL, + 'capability' => 'text→text', + 'description' => __( 'Model for LLM Deep Research. Pick a reasoning-capable model (e.g. Grok 4.1 Fast, DeepSeek-R1) for best results. Only used when LLM Deep Research is enabled above.', 'prautoblogger' ), + 'badge' => __( 'Reasoning', 'prautoblogger' ), + ], + [ + 'id' => 'prautoblogger_research_prompt', + 'label' => __( 'Research Prompt', 'prautoblogger' ), + 'type' => 'textarea', + 'section' => 'prautoblogger_sources', + 'default' => 'Conduct a deep research sweep of the {niche} space and identify the most substantive, actionable topics that practitioners and curious newcomers are searching for or actively discussing.' . "\n\n" . 'Focus on:' . "\n" . '- Practical questions real readers would search for (not theoretical debates)' . "\n" . '- Emerging trends gaining momentum in the community' . "\n" . '- Common misconceptions that deserve clarification' . "\n" . '- Comparative analysis (product vs. product, protocol vs. protocol)' . "\n" . '- Safety and efficacy topics that drive concern or curiosity' . "\n" . '- Beginner-to-advanced mix — cover entry-level and depth-focused topics' . "\n\n" . 'Aim for topics that feel timely, underserved by existing content, and likely to drive reader engagement. Return 8-12 findings with detailed analysis in each.', + 'description' => __( 'The research brief sent to the LLM. Use {niche} as a placeholder for your niche description. The system prompt handles output format — this controls WHAT to research.', 'prautoblogger' ), + ], + // ── AI Models (extended) ──────────────────────────────────── [ 'id' => 'prautoblogger_reasoning_enabled', diff --git a/includes/admin/class-settings-fields.php b/includes/admin/class-settings-fields.php index d9c2667..96a3fb4 100755 --- a/includes/admin/class-settings-fields.php +++ b/includes/admin/class-settings-fields.php @@ -248,7 +248,8 @@ class PRAutoBlogger_Settings_Fields { 'type' => 'checkboxes', 'section' => 'prautoblogger_sources', 'options' => [ - 'reddit' => __( 'Reddit (RSS + .json)', 'prautoblogger' ), + 'reddit' => __( 'Reddit (RSS + .json)', 'prautoblogger' ), + 'llm_research' => __( 'LLM Deep Research (reasoning model)', 'prautoblogger' ), ], 'default' => '["reddit"]', 'description' => __( 'Select which platforms to monitor for topics.', 'prautoblogger' ), diff --git a/includes/core/class-content-analyzer.php b/includes/core/class-content-analyzer.php index b09ade3..877a48f 100755 --- a/includes/core/class-content-analyzer.php +++ b/includes/core/class-content-analyzer.php @@ -147,12 +147,14 @@ class PRAutoBlogger_Content_Analyzer { $current_chars = 0; foreach ( $items as $item ) { + // LLM research findings have no subreddit; use a cleaner label. + $source_label = 'llm_research' === $item['source_type'] + ? sprintf( '[LLM Research] Relevance: %d', (int) $item['score'] ) + : sprintf( '[%s] r/%s | Score: %d | Comments: %d', $item['source_type'], $item['subreddit'] ?? 'unknown', (int) $item['score'], (int) $item['comment_count'] ); + $entry = sprintf( - "[%s] r/%s | Score: %d | Comments: %d\nTitle: %s\n%s\n---", - $item['source_type'], - $item['subreddit'] ?? 'unknown', - (int) $item['score'], - (int) $item['comment_count'], + "%s\nTitle: %s\n%s\n---", + $source_label, $item['title'] ?? '(comment)', mb_substr( $item['content'] ?? '', 0, 500 ) ); diff --git a/includes/core/class-source-collector.php b/includes/core/class-source-collector.php index 9b53595..8b6a0b7 100755 --- a/includes/core/class-source-collector.php +++ b/includes/core/class-source-collector.php @@ -4,7 +4,7 @@ declare(strict_types=1); /** * Orchestrates data collection from all enabled source providers. * - * Iterates through enabled sources (currently Reddit), calls their + * Iterates through enabled sources (Reddit, LLM Research), calls their * collect_data() method, and stores results in the ab_source_data table. * Handles deduplication via UNIQUE index on (source_type, source_id). * Re-seen items get their collected_at timestamp refreshed so the @@ -14,7 +14,8 @@ declare(strict_types=1); * Dependencies: Source provider implementations, WordPress $wpdb. * * @see providers/interface-source-provider.php — Interface providers implement. - * @see providers/class-reddit-provider.php — Primary source provider. + * @see providers/class-reddit-provider.php — Reddit source provider. + * @see providers/class-llm-research-provider.php — LLM deep research provider. * @see ARCHITECTURE.md — Data flow step 1. */ class PRAutoBlogger_Source_Collector { @@ -169,8 +170,8 @@ class PRAutoBlogger_Source_Collector { */ private function get_provider( string $source_type ): ?PRAutoBlogger_Source_Provider_Interface { $providers = [ - 'reddit' => PRAutoBlogger_Reddit_Provider::class, - // New providers can be added here — see CONVENTIONS.md for how-to. + 'reddit' => PRAutoBlogger_Reddit_Provider::class, + 'llm_research' => PRAutoBlogger_LLM_Research_Provider::class, ]; /** @@ -209,14 +210,21 @@ class PRAutoBlogger_Source_Collector { private function get_config_for_source( string $source_type ): array { if ( 'reddit' === $source_type ) { return [ - 'subreddits' => json_decode( get_option( 'prautoblogger_target_subreddits', '[]' ), true ) ?: [], - 'limit' => 25, - 'time_filter' => 'day', - 'include_comments' => true, + 'subreddits' => json_decode( get_option( 'prautoblogger_target_subreddits', '[]' ), true ) ?: [], + 'limit' => 25, + 'time_filter' => 'day', + 'include_comments' => true, 'comments_per_post' => 10, ]; } + if ( 'llm_research' === $source_type ) { + return [ + 'model' => get_option( 'prautoblogger_research_model', PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL ), + 'prompt' => get_option( 'prautoblogger_research_prompt', '' ), + ]; + } + return []; } } diff --git a/includes/providers/class-llm-research-provider.php b/includes/providers/class-llm-research-provider.php new file mode 100644 index 0000000..78571a4 --- /dev/null +++ b/includes/providers/class-llm-research-provider.php @@ -0,0 +1,211 @@ +warning( 'LLM Research prompt is empty. Skipping.', 'llm-research' ); + return []; + } + + $system_prompt = $this->build_system_prompt(); + $user_prompt = $this->build_user_prompt( $prompt, $niche ); + + $llm = new PRAutoBlogger_OpenRouter_Provider(); + $response = $llm->send_chat_completion( + [ + [ 'role' => 'system', 'content' => $system_prompt ], + [ 'role' => 'user', 'content' => $user_prompt ], + ], + $model, + [ + 'temperature' => 0.7, + 'max_tokens' => 8000, + 'response_format' => [ 'type' => 'json_object' ], + ] + ); + + $cost_tracker = new PRAutoBlogger_Cost_Tracker(); + $cost_tracker->log_api_call( + null, + 'llm_research', + $llm->get_provider_name(), + $response['model'], + $response['prompt_tokens'], + $response['completion_tokens'] + ); + + return $this->parse_findings( $response['content'], $response['model'] ); + } + + /** + * @return string + */ + public function get_source_type(): string { + return self::SOURCE_TYPE; + } + + /** + * Validate credentials by delegating to the OpenRouter provider. + * + * @return bool + */ + public function validate_credentials(): bool { + return ( new PRAutoBlogger_OpenRouter_Provider() )->validate_credentials(); + } + + /** + * LLM research has no rate limits beyond the OpenRouter provider's own. + * + * @return array{remaining: int, limit: int, resets_at: string} + */ + public function get_rate_limit_status(): array { + return [ + 'remaining' => 999, + 'limit' => 999, + 'resets_at' => '', + ]; + } + + /** + * Build the system prompt for the research LLM. + * + * @return string + */ + private function build_system_prompt(): string { + return 'You are a deep research analyst specializing in emerging trends, ' + . 'scientific developments, and community knowledge in niche health and ' + . "biohacking domains. Your task is to identify substantive, actionable findings.\n\n" + . "Using your training knowledge, scan across:\n" + . "- Recent scientific literature and research directions\n" + . "- Active community discussions, debates, and evolving consensus\n" + . "- Emerging products, protocols, and methodologies gaining traction\n" + . "- Common questions, misconceptions, and points of confusion\n" + . "- Regulatory changes, safety concerns, and legal shifts\n" + . "- Gaps between what practitioners want to know and what content exists\n\n" + . "Your findings must be:\n" + . "1. SUBSTANTIVE — 2-3 paragraphs of detailed analysis per finding, not a headline\n" + . "2. ACTIONABLE — practical enough to seed a full article\n" + . "3. REALISTIC — grounded in what is actually discussed or researched; do NOT invent citations\n" + . "4. TIMELY — reflect current knowledge, recent shifts, or emerging questions\n\n" + . "Respond with valid JSON only (no preamble, no markdown fences):\n" + . '{"findings": [{"title": "string", "content": "string (2-3 paragraphs)", ' + . '"relevance_score": 0-100, "category": "question|trend|comparison|guide|misconception|safety"}]}' . "\n\n" + . "Generate 8-12 findings. Prioritize depth over breadth. Avoid generic or surface-level topics."; + } + + /** + * Build the user prompt by injecting the niche into the configured template. + * + * @param string $prompt_template User-configured research prompt. + * @param string $niche Niche description from settings. + * @return string + */ + private function build_user_prompt( string $prompt_template, string $niche ): string { + $niche_label = '' !== $niche ? $niche : 'your domain'; + return str_replace( '{niche}', $niche_label, $prompt_template ); + } + + /** + * Parse the LLM's JSON response into Source_Data objects. + * + * @param string $content Raw LLM response (JSON). + * @param string $model Model that produced the response (stored as author). + * @return PRAutoBlogger_Source_Data[] + */ + private function parse_findings( string $content, string $model ): array { + $data = PRAutoBlogger_Json_Extractor::decode( $content ); + + if ( ! isset( $data['findings'] ) || ! is_array( $data['findings'] ) ) { + PRAutoBlogger_Logger::instance()->error( + 'LLM Research response was not valid JSON or missing findings key. Raw (first 500 chars): ' + . mb_substr( $content, 0, 500 ), + 'llm-research' + ); + return []; + } + + $items = []; + $run_date = gmdate( 'Y-m-d' ); + + foreach ( $data['findings'] as $index => $finding ) { + $title = sanitize_text_field( $finding['title'] ?? '' ); + $body = sanitize_textarea_field( $finding['content'] ?? '' ); + $score = min( 100, max( 0, (int) ( $finding['relevance_score'] ?? 50 ) ) ); + $category = sanitize_text_field( $finding['category'] ?? 'guide' ); + + if ( '' === $title || '' === $body ) { + continue; + } + + // source_id uses date + index so the same day's run deduplicates + // but a new day produces fresh entries. + $source_id = 'llm_' . $run_date . '_' . $index; + + $items[] = new PRAutoBlogger_Source_Data( [ + 'source_type' => self::SOURCE_TYPE, + 'source_id' => $source_id, + 'subreddit' => null, + 'title' => $title, + 'content' => $body, + 'author' => $model, + 'score' => $score, + 'comment_count' => 0, + 'permalink' => null, + 'collected_at' => current_time( 'mysql' ), + 'metadata' => [ + 'category' => $category, + 'data_source' => 'llm_research', + 'model' => $model, + ], + ] ); + } + + PRAutoBlogger_Logger::instance()->info( + sprintf( 'LLM Research produced %d findings via %s.', count( $items ), $model ), + 'llm-research' + ); + + return $items; + } +} diff --git a/prautoblogger.php b/prautoblogger.php index d2e5859..a1795fc 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.4.9 + * Version: 0.5.0 * 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.4.9' ); +define( 'PRAUTOBLOGGER_VERSION', '0.5.0' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );