Initial commit: AutoBlogger v0.1.0
WordPress plugin that auto-researches topics from Reddit, generates SEO-optimized articles via OpenRouter LLMs, runs them through a chief editor agent, and publishes on a daily schedule. Includes: - Full generation pipeline (collect → analyze → score → generate → review → publish) - Cost tracking and budget enforcement - Admin UI: settings, review queue, metrics, activity log - CI/CD pipeline (GitHub Actions → WordPress REST deploy) - Unit test suite (PHPUnit + Brain Monkey) - Structured logging with configurable verbosity Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
102
includes/models/class-analysis-result.php
Executable file
102
includes/models/class-analysis-result.php
Executable file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Value object representing an analyzed topic pattern (recurring question,
|
||||
* complaint, or comparison detected in source data).
|
||||
*
|
||||
* Triggered by: Content_Analyzer creates these; Idea_Scorer consumes them.
|
||||
* Dependencies: None — pure data object.
|
||||
*
|
||||
* @see core/class-content-analyzer.php — Creates these from source data + LLM analysis.
|
||||
* @see core/class-idea-scorer.php — Ranks these into article ideas.
|
||||
*/
|
||||
class Autoblogger_Analysis_Result {
|
||||
|
||||
private int $id;
|
||||
private string $analysis_type;
|
||||
private string $topic;
|
||||
private ?string $summary;
|
||||
private int $frequency;
|
||||
private float $relevance_score;
|
||||
private array $source_ids;
|
||||
private string $analyzed_at;
|
||||
private ?array $metadata;
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* id?: int,
|
||||
* analysis_type: string,
|
||||
* topic: string,
|
||||
* summary?: string|null,
|
||||
* frequency?: int,
|
||||
* relevance_score?: float,
|
||||
* source_ids?: int[],
|
||||
* analyzed_at?: string,
|
||||
* metadata?: array|null,
|
||||
* } $data
|
||||
*/
|
||||
public function __construct( array $data ) {
|
||||
$this->id = (int) ( $data['id'] ?? 0 );
|
||||
$this->analysis_type = $data['analysis_type'];
|
||||
$this->topic = $data['topic'];
|
||||
$this->summary = $data['summary'] ?? null;
|
||||
$this->frequency = (int) ( $data['frequency'] ?? 1 );
|
||||
$this->relevance_score = (float) ( $data['relevance_score'] ?? 0.0 );
|
||||
$this->source_ids = $data['source_ids'] ?? [];
|
||||
$this->analyzed_at = $data['analyzed_at'] ?? current_time( 'mysql' );
|
||||
$this->metadata = $data['metadata'] ?? null;
|
||||
}
|
||||
|
||||
public function get_id(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function get_analysis_type(): string {
|
||||
return $this->analysis_type;
|
||||
}
|
||||
|
||||
public function get_topic(): string {
|
||||
return $this->topic;
|
||||
}
|
||||
|
||||
public function get_summary(): ?string {
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function get_frequency(): int {
|
||||
return $this->frequency;
|
||||
}
|
||||
|
||||
public function get_relevance_score(): float {
|
||||
return $this->relevance_score;
|
||||
}
|
||||
|
||||
public function get_source_ids(): array {
|
||||
return $this->source_ids;
|
||||
}
|
||||
|
||||
public function get_analyzed_at(): string {
|
||||
return $this->analyzed_at;
|
||||
}
|
||||
|
||||
public function get_metadata(): ?array {
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function to_db_row(): array {
|
||||
return [
|
||||
'analysis_type' => $this->analysis_type,
|
||||
'topic' => $this->topic,
|
||||
'summary' => $this->summary,
|
||||
'frequency' => $this->frequency,
|
||||
'relevance_score' => $this->relevance_score,
|
||||
'source_ids_json' => wp_json_encode( $this->source_ids ),
|
||||
'analyzed_at' => $this->analyzed_at,
|
||||
'metadata_json' => null !== $this->metadata ? wp_json_encode( $this->metadata ) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
87
includes/models/class-article-idea.php
Executable file
87
includes/models/class-article-idea.php
Executable file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Value object representing a scored article idea ready for content generation.
|
||||
*
|
||||
* Created by the Idea_Scorer from Analysis_Results. Consumed by Content_Generator.
|
||||
*
|
||||
* Triggered by: Idea_Scorer creates these; Content_Generator and Chief_Editor consume them.
|
||||
* Dependencies: None — pure data object.
|
||||
*
|
||||
* @see core/class-idea-scorer.php — Creates and ranks these.
|
||||
* @see core/class-content-generator.php — Uses these as generation input.
|
||||
*/
|
||||
class Autoblogger_Article_Idea {
|
||||
|
||||
private string $topic;
|
||||
private string $article_type;
|
||||
private string $suggested_title;
|
||||
private string $summary;
|
||||
private float $score;
|
||||
private int $analysis_id;
|
||||
private array $source_ids;
|
||||
private array $key_points;
|
||||
private array $target_keywords;
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* topic: string,
|
||||
* article_type: string,
|
||||
* suggested_title: string,
|
||||
* summary: string,
|
||||
* score?: float,
|
||||
* analysis_id?: int,
|
||||
* source_ids?: int[],
|
||||
* key_points?: string[],
|
||||
* target_keywords?: string[],
|
||||
* } $data
|
||||
*/
|
||||
public function __construct( array $data ) {
|
||||
$this->topic = $data['topic'];
|
||||
$this->article_type = $data['article_type'];
|
||||
$this->suggested_title = $data['suggested_title'];
|
||||
$this->summary = $data['summary'];
|
||||
$this->score = (float) ( $data['score'] ?? 0.0 );
|
||||
$this->analysis_id = (int) ( $data['analysis_id'] ?? 0 );
|
||||
$this->source_ids = $data['source_ids'] ?? [];
|
||||
$this->key_points = $data['key_points'] ?? [];
|
||||
$this->target_keywords = $data['target_keywords'] ?? [];
|
||||
}
|
||||
|
||||
public function get_topic(): string {
|
||||
return $this->topic;
|
||||
}
|
||||
|
||||
public function get_article_type(): string {
|
||||
return $this->article_type;
|
||||
}
|
||||
|
||||
public function get_suggested_title(): string {
|
||||
return $this->suggested_title;
|
||||
}
|
||||
|
||||
public function get_summary(): string {
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function get_score(): float {
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
public function get_analysis_id(): int {
|
||||
return $this->analysis_id;
|
||||
}
|
||||
|
||||
public function get_source_ids(): array {
|
||||
return $this->source_ids;
|
||||
}
|
||||
|
||||
public function get_key_points(): array {
|
||||
return $this->key_points;
|
||||
}
|
||||
|
||||
public function get_target_keywords(): array {
|
||||
return $this->target_keywords;
|
||||
}
|
||||
}
|
||||
70
includes/models/class-content-request.php
Executable file
70
includes/models/class-content-request.php
Executable file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Value object encapsulating all parameters for a content generation request.
|
||||
*
|
||||
* Bundles the article idea with user-configured generation preferences
|
||||
* (tone, word count, pipeline mode) into a single object passed through the pipeline.
|
||||
*
|
||||
* Triggered by: Content_Generator creates these from Article_Ideas + settings.
|
||||
* Dependencies: None — pure data object.
|
||||
*
|
||||
* @see core/class-content-generator.php — Creates and consumes these.
|
||||
*/
|
||||
class Autoblogger_Content_Request {
|
||||
|
||||
private Autoblogger_Article_Idea $idea;
|
||||
private string $pipeline_mode;
|
||||
private string $tone;
|
||||
private int $min_word_count;
|
||||
private int $max_word_count;
|
||||
private string $niche_description;
|
||||
private array $topic_exclusions;
|
||||
|
||||
public function __construct(
|
||||
Autoblogger_Article_Idea $idea,
|
||||
string $pipeline_mode,
|
||||
string $tone,
|
||||
int $min_word_count,
|
||||
int $max_word_count,
|
||||
string $niche_description,
|
||||
array $topic_exclusions = []
|
||||
) {
|
||||
$this->idea = $idea;
|
||||
$this->pipeline_mode = $pipeline_mode;
|
||||
$this->tone = $tone;
|
||||
$this->min_word_count = $min_word_count;
|
||||
$this->max_word_count = $max_word_count;
|
||||
$this->niche_description = $niche_description;
|
||||
$this->topic_exclusions = $topic_exclusions;
|
||||
}
|
||||
|
||||
public function get_idea(): Autoblogger_Article_Idea {
|
||||
return $this->idea;
|
||||
}
|
||||
|
||||
public function get_pipeline_mode(): string {
|
||||
return $this->pipeline_mode;
|
||||
}
|
||||
|
||||
public function get_tone(): string {
|
||||
return $this->tone;
|
||||
}
|
||||
|
||||
public function get_min_word_count(): int {
|
||||
return $this->min_word_count;
|
||||
}
|
||||
|
||||
public function get_max_word_count(): int {
|
||||
return $this->max_word_count;
|
||||
}
|
||||
|
||||
public function get_niche_description(): string {
|
||||
return $this->niche_description;
|
||||
}
|
||||
|
||||
public function get_topic_exclusions(): array {
|
||||
return $this->topic_exclusions;
|
||||
}
|
||||
}
|
||||
65
includes/models/class-content-score.php
Executable file
65
includes/models/class-content-score.php
Executable file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Value object for a post's composite performance score.
|
||||
*
|
||||
* Triggered by: Metrics_Collector creates these; Content_Analyzer reads them for self-improvement.
|
||||
* Dependencies: None — pure data object.
|
||||
*
|
||||
* @see core/class-metrics-collector.php — Creates these from WP + GA4 data.
|
||||
* @see core/class-content-analyzer.php — Reads past scores to refine analysis.
|
||||
*/
|
||||
class Autoblogger_Content_Score {
|
||||
|
||||
private int $id;
|
||||
private int $post_id;
|
||||
private int $pageviews;
|
||||
private float $avg_time_on_page;
|
||||
private float $bounce_rate;
|
||||
private int $comment_count;
|
||||
private float $composite_score;
|
||||
private ?array $score_factors;
|
||||
private string $measured_at;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function __construct( array $data ) {
|
||||
$this->id = (int) ( $data['id'] ?? 0 );
|
||||
$this->post_id = (int) ( $data['post_id'] ?? 0 );
|
||||
$this->pageviews = (int) ( $data['pageviews'] ?? 0 );
|
||||
$this->avg_time_on_page = (float) ( $data['avg_time_on_page'] ?? 0.0 );
|
||||
$this->bounce_rate = (float) ( $data['bounce_rate'] ?? 0.0 );
|
||||
$this->comment_count = (int) ( $data['comment_count'] ?? 0 );
|
||||
$this->composite_score = (float) ( $data['composite_score'] ?? 0.0 );
|
||||
$this->score_factors = $data['score_factors'] ?? null;
|
||||
$this->measured_at = $data['measured_at'] ?? current_time( 'mysql' );
|
||||
}
|
||||
|
||||
public function get_id(): int { return $this->id; }
|
||||
public function get_post_id(): int { return $this->post_id; }
|
||||
public function get_pageviews(): int { return $this->pageviews; }
|
||||
public function get_avg_time_on_page(): float { return $this->avg_time_on_page; }
|
||||
public function get_bounce_rate(): float { return $this->bounce_rate; }
|
||||
public function get_comment_count(): int { return $this->comment_count; }
|
||||
public function get_composite_score(): float { return $this->composite_score; }
|
||||
public function get_score_factors(): ?array { return $this->score_factors; }
|
||||
public function get_measured_at(): string { return $this->measured_at; }
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function to_db_row(): array {
|
||||
return [
|
||||
'post_id' => $this->post_id,
|
||||
'pageviews' => $this->pageviews,
|
||||
'avg_time_on_page' => $this->avg_time_on_page,
|
||||
'bounce_rate' => $this->bounce_rate,
|
||||
'comment_count' => $this->comment_count,
|
||||
'composite_score' => $this->composite_score,
|
||||
'score_factors_json' => null !== $this->score_factors ? wp_json_encode( $this->score_factors ) : null,
|
||||
'measured_at' => $this->measured_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
64
includes/models/class-editorial-review.php
Executable file
64
includes/models/class-editorial-review.php
Executable file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Value object representing the chief editor agent's review of generated content.
|
||||
*
|
||||
* Triggered by: Chief_Editor creates these; Publisher consumes them.
|
||||
* Dependencies: None — pure data object.
|
||||
*
|
||||
* @see core/class-chief-editor.php — Creates these after reviewing content.
|
||||
* @see core/class-publisher.php — Uses verdict to decide publish vs. draft.
|
||||
*/
|
||||
class Autoblogger_Editorial_Review {
|
||||
|
||||
private string $verdict;
|
||||
private string $notes;
|
||||
private ?string $revised_content;
|
||||
private float $quality_score;
|
||||
private float $seo_score;
|
||||
private array $issues;
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* verdict: string,
|
||||
* notes: string,
|
||||
* revised_content?: string|null,
|
||||
* quality_score?: float,
|
||||
* seo_score?: float,
|
||||
* issues?: string[],
|
||||
* } $data
|
||||
*/
|
||||
public function __construct( array $data ) {
|
||||
$this->verdict = $data['verdict']; // 'approved', 'revised', 'rejected'
|
||||
$this->notes = $data['notes'];
|
||||
$this->revised_content = $data['revised_content'] ?? null;
|
||||
$this->quality_score = (float) ( $data['quality_score'] ?? 0.0 );
|
||||
$this->seo_score = (float) ( $data['seo_score'] ?? 0.0 );
|
||||
$this->issues = $data['issues'] ?? [];
|
||||
}
|
||||
|
||||
public function get_verdict(): string {
|
||||
return $this->verdict;
|
||||
}
|
||||
|
||||
public function get_notes(): string {
|
||||
return $this->notes;
|
||||
}
|
||||
|
||||
public function get_revised_content(): ?string {
|
||||
return $this->revised_content;
|
||||
}
|
||||
|
||||
public function get_quality_score(): float {
|
||||
return $this->quality_score;
|
||||
}
|
||||
|
||||
public function get_seo_score(): float {
|
||||
return $this->seo_score;
|
||||
}
|
||||
|
||||
public function get_issues(): array {
|
||||
return $this->issues;
|
||||
}
|
||||
}
|
||||
81
includes/models/class-generation-log.php
Executable file
81
includes/models/class-generation-log.php
Executable file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Value object for an API call log entry (cost tracking).
|
||||
*
|
||||
* Triggered by: Cost_Tracker creates these; Metrics_Page displays them.
|
||||
* Dependencies: None — pure data object.
|
||||
*
|
||||
* @see core/class-cost-tracker.php — Writes these to ab_generation_log table.
|
||||
* @see admin/class-metrics-page.php — Reads and displays these.
|
||||
*/
|
||||
class Autoblogger_Generation_Log {
|
||||
|
||||
private int $id;
|
||||
private ?int $post_id;
|
||||
private ?string $run_id;
|
||||
private string $stage;
|
||||
private string $provider;
|
||||
private string $model;
|
||||
private int $prompt_tokens;
|
||||
private int $completion_tokens;
|
||||
private float $estimated_cost;
|
||||
private ?string $request_json;
|
||||
private string $response_status;
|
||||
private ?string $error_message;
|
||||
private string $created_at;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function __construct( array $data ) {
|
||||
$this->id = (int) ( $data['id'] ?? 0 );
|
||||
$this->post_id = isset( $data['post_id'] ) ? (int) $data['post_id'] : null;
|
||||
$this->run_id = $data['run_id'] ?? null;
|
||||
$this->stage = $data['stage'] ?? '';
|
||||
$this->provider = $data['provider'] ?? '';
|
||||
$this->model = $data['model'] ?? '';
|
||||
$this->prompt_tokens = (int) ( $data['prompt_tokens'] ?? 0 );
|
||||
$this->completion_tokens = (int) ( $data['completion_tokens'] ?? 0 );
|
||||
$this->estimated_cost = (float) ( $data['estimated_cost'] ?? 0.0 );
|
||||
$this->request_json = $data['request_json'] ?? null;
|
||||
$this->response_status = $data['response_status'] ?? 'success';
|
||||
$this->error_message = $data['error_message'] ?? null;
|
||||
$this->created_at = $data['created_at'] ?? current_time( 'mysql' );
|
||||
}
|
||||
|
||||
public function get_id(): int { return $this->id; }
|
||||
public function get_post_id(): ?int { return $this->post_id; }
|
||||
public function get_run_id(): ?string { return $this->run_id; }
|
||||
public function get_stage(): string { return $this->stage; }
|
||||
public function get_provider(): string { return $this->provider; }
|
||||
public function get_model(): string { return $this->model; }
|
||||
public function get_prompt_tokens(): int { return $this->prompt_tokens; }
|
||||
public function get_completion_tokens(): int { return $this->completion_tokens; }
|
||||
public function get_estimated_cost(): float { return $this->estimated_cost; }
|
||||
public function get_request_json(): ?string { return $this->request_json; }
|
||||
public function get_response_status(): string { return $this->response_status; }
|
||||
public function get_error_message(): ?string { return $this->error_message; }
|
||||
public function get_created_at(): string { return $this->created_at; }
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function to_db_row(): array {
|
||||
return [
|
||||
'post_id' => $this->post_id,
|
||||
'run_id' => $this->run_id,
|
||||
'stage' => $this->stage,
|
||||
'provider' => $this->provider,
|
||||
'model' => $this->model,
|
||||
'prompt_tokens' => $this->prompt_tokens,
|
||||
'completion_tokens' => $this->completion_tokens,
|
||||
'estimated_cost' => $this->estimated_cost,
|
||||
'request_json' => $this->request_json,
|
||||
'response_status' => $this->response_status,
|
||||
'error_message' => $this->error_message,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
128
includes/models/class-source-data.php
Executable file
128
includes/models/class-source-data.php
Executable file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Value object representing a single piece of collected social media data
|
||||
* (a Reddit post, a TikTok comment, etc.).
|
||||
*
|
||||
* Triggered by: Source providers create these; Content_Analyzer consumes them.
|
||||
* Dependencies: None — pure data object.
|
||||
*
|
||||
* @see providers/interface-source-provider.php — Providers return arrays of these.
|
||||
* @see core/class-content-analyzer.php — Consumes these for pattern detection.
|
||||
*/
|
||||
class Autoblogger_Source_Data {
|
||||
|
||||
private int $id;
|
||||
private string $source_type;
|
||||
private string $source_id;
|
||||
private ?string $subreddit;
|
||||
private ?string $title;
|
||||
private ?string $content;
|
||||
private ?string $author;
|
||||
private int $score;
|
||||
private int $comment_count;
|
||||
private ?string $permalink;
|
||||
private string $collected_at;
|
||||
private ?array $metadata;
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* id?: int,
|
||||
* source_type: string,
|
||||
* source_id: string,
|
||||
* subreddit?: string|null,
|
||||
* title?: string|null,
|
||||
* content?: string|null,
|
||||
* author?: string|null,
|
||||
* score?: int,
|
||||
* comment_count?: int,
|
||||
* permalink?: string|null,
|
||||
* collected_at?: string,
|
||||
* metadata?: array|null,
|
||||
* } $data
|
||||
*/
|
||||
public function __construct( array $data ) {
|
||||
$this->id = (int) ( $data['id'] ?? 0 );
|
||||
$this->source_type = $data['source_type'];
|
||||
$this->source_id = $data['source_id'];
|
||||
$this->subreddit = $data['subreddit'] ?? null;
|
||||
$this->title = $data['title'] ?? null;
|
||||
$this->content = $data['content'] ?? null;
|
||||
$this->author = $data['author'] ?? null;
|
||||
$this->score = (int) ( $data['score'] ?? 0 );
|
||||
$this->comment_count = (int) ( $data['comment_count'] ?? 0 );
|
||||
$this->permalink = $data['permalink'] ?? null;
|
||||
$this->collected_at = $data['collected_at'] ?? current_time( 'mysql' );
|
||||
$this->metadata = $data['metadata'] ?? null;
|
||||
}
|
||||
|
||||
public function get_id(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function get_source_type(): string {
|
||||
return $this->source_type;
|
||||
}
|
||||
|
||||
public function get_source_id(): string {
|
||||
return $this->source_id;
|
||||
}
|
||||
|
||||
public function get_subreddit(): ?string {
|
||||
return $this->subreddit;
|
||||
}
|
||||
|
||||
public function get_title(): ?string {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function get_content(): ?string {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function get_author(): ?string {
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function get_score(): int {
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
public function get_comment_count(): int {
|
||||
return $this->comment_count;
|
||||
}
|
||||
|
||||
public function get_permalink(): ?string {
|
||||
return $this->permalink;
|
||||
}
|
||||
|
||||
public function get_collected_at(): string {
|
||||
return $this->collected_at;
|
||||
}
|
||||
|
||||
public function get_metadata(): ?array {
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for database insertion.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function to_db_row(): array {
|
||||
return [
|
||||
'source_type' => $this->source_type,
|
||||
'source_id' => $this->source_id,
|
||||
'subreddit' => $this->subreddit,
|
||||
'title' => $this->title,
|
||||
'content' => $this->content,
|
||||
'author' => $this->author,
|
||||
'score' => $this->score,
|
||||
'comment_count' => $this->comment_count,
|
||||
'permalink' => $this->permalink,
|
||||
'collected_at' => $this->collected_at,
|
||||
'metadata_json' => null !== $this->metadata ? wp_json_encode( $this->metadata ) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user