feat: semantic dedup + LLM-aware topic avoidance
Two-pronged fix for repeated article topics: 1. Feed last 30 days of published article titles into the LLM analysis prompt with explicit "avoid these" instruction. Zero cost — just extra context in an existing API call. 2. Replace keyword-overlap dedup (60% word match) with embedding-based cosine similarity via sentence-transformers/all-MiniLM-L12-v2 on OpenRouter. Catches rephrasings that keyword matching misses (e.g. "BPC-157 dosing" vs "how much BPC-157 to take"). Falls back to keyword matching if the embedding API is unavailable. Cost: ~$0.00001 per generation run ($0.02/M tokens, ~500 tokens/run). New files: - class-semantic-dedup.php — Embedding cosine similarity + keyword fallback - class-open-router-embedding-provider.php — OpenRouter /embeddings client Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,11 @@ class PRAutoBlogger_Analysis_Prompts {
|
||||
$prompt .= $performance_context . "\n\n";
|
||||
}
|
||||
|
||||
$recent_context = self::get_recent_articles_context();
|
||||
if ( '' !== $recent_context ) {
|
||||
$prompt .= $recent_context . "\n\n";
|
||||
}
|
||||
|
||||
$prompt .= "Respond with valid JSON containing exactly {$target_count} patterns:\n";
|
||||
$prompt .= '{"patterns": [{"type": "...", "topic": "...", "summary": "...", "frequency": N, "relevance_score": 0.X, "suggested_title": "...", "key_points": [...], "target_keywords": [...]}]}';
|
||||
|
||||
@@ -112,4 +117,43 @@ class PRAutoBlogger_Analysis_Prompts {
|
||||
|
||||
return implode( "\n", $lines );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get titles of recently published articles so the LLM avoids repeating them.
|
||||
*
|
||||
* Fetches generated posts from the last 30 days. A wider window than the
|
||||
* scorer's 7-day dedup because the LLM is smart enough to find genuinely
|
||||
* new angles even on adjacent topics — we just need it to know what exists.
|
||||
*
|
||||
* Side effects: database read via WP_Query.
|
||||
*
|
||||
* @return string Formatted context block, or empty string if no recent articles.
|
||||
*/
|
||||
public static function get_recent_articles_context(): string {
|
||||
$query = new \WP_Query( [
|
||||
'post_type' => 'post',
|
||||
'post_status' => [ 'publish', 'draft', 'pending' ],
|
||||
'meta_key' => '_prautoblogger_generated',
|
||||
'meta_value' => '1',
|
||||
'date_query' => [ [ 'after' => '30 days ago' ] ],
|
||||
'posts_per_page' => 100,
|
||||
'fields' => 'ids',
|
||||
'no_found_rows' => true,
|
||||
] );
|
||||
|
||||
if ( empty( $query->posts ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$titles = [];
|
||||
foreach ( $query->posts as $post_id ) {
|
||||
$titles[] = '- "' . get_the_title( $post_id ) . '"';
|
||||
}
|
||||
|
||||
$block = "IMPORTANT — Topics already covered (last 30 days). Do NOT suggest topics that overlap with these. ";
|
||||
$block .= "Find genuinely NEW angles, questions, or subjects that are NOT already covered:\n";
|
||||
$block .= implode( "\n", $titles );
|
||||
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user