feat: add LLM Deep Research source provider (v0.5.0)
New data source that sends a configurable research prompt to a reasoning-capable model (Grok 4.1 Fast, DeepSeek-R1, etc.) to identify trending topics, content gaps, and emerging questions. Findings feed into the content analyzer alongside Reddit data, combining real-time community signals with deep knowledge-base research. New admin settings: Research Model picker, Research Prompt textarea, and LLM Deep Research toggle in Enabled Sources. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
19
CHANGELOG.md
19
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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' ),
|
||||
|
||||
@@ -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 )
|
||||
);
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
211
includes/providers/class-llm-research-provider.php
Normal file
211
includes/providers/class-llm-research-provider.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* LLM Deep Research source provider — uses a reasoning-capable model to
|
||||
* identify trending topics, emerging questions, and content gaps in the niche.
|
||||
*
|
||||
* Unlike the Reddit provider (which scrapes real-time discussions), this
|
||||
* provider leverages an LLM's training knowledge to surface topics that
|
||||
* community data might miss: recent research directions, common
|
||||
* misconceptions, safety concerns, and underserved content gaps.
|
||||
*
|
||||
* Findings are stored as Source_Data objects alongside Reddit data so the
|
||||
* Content_Analyzer sees both signals when generating article ideas.
|
||||
*
|
||||
* Triggered by: Source_Collector::collect_from_all_sources() when 'llm_research' is enabled.
|
||||
* Dependencies: PRAutoBlogger_OpenRouter_Provider (LLM calls), PRAutoBlogger_Cost_Tracker.
|
||||
*
|
||||
* @see providers/interface-source-provider.php — Interface this class implements.
|
||||
* @see core/class-source-collector.php — Orchestrates collection from all providers.
|
||||
* @see core/class-content-analyzer.php — Consumes research findings for idea generation.
|
||||
* @see ARCHITECTURE.md — Data flow step 1.
|
||||
*/
|
||||
class PRAutoBlogger_LLM_Research_Provider implements PRAutoBlogger_Source_Provider_Interface {
|
||||
|
||||
private const SOURCE_TYPE = 'llm_research';
|
||||
|
||||
/**
|
||||
* Collect research findings by sending the configured prompt to an LLM.
|
||||
*
|
||||
* Sends the system prompt + user research prompt to the selected model,
|
||||
* parses the JSON response, and returns each finding as a Source_Data object.
|
||||
* Cost is tracked via the shared Cost_Tracker.
|
||||
*
|
||||
* Side effects: LLM API call via OpenRouter, cost logging.
|
||||
*
|
||||
* @param array{prompt?: string, model?: string} $config Research-specific configuration.
|
||||
* @return PRAutoBlogger_Source_Data[] Array of research findings as source data.
|
||||
* @throws \RuntimeException On API error after retries.
|
||||
*/
|
||||
public function collect_data( array $config ): array {
|
||||
$model = $config['model'] ?? get_option( 'prautoblogger_research_model', PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL );
|
||||
$prompt = $config['prompt'] ?? get_option( 'prautoblogger_research_prompt', '' );
|
||||
$niche = get_option( 'prautoblogger_niche_description', '' );
|
||||
|
||||
if ( '' === trim( $prompt ) ) {
|
||||
PRAutoBlogger_Logger::instance()->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;
|
||||
}
|
||||
}
|
||||
@@ -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__ ) );
|
||||
|
||||
Reference in New Issue
Block a user