Add category management - Create and update WordPress categories from AI
New Features: - Create WordPress categories based on AI proposals - Bulk assign posts to categories - Confidence-based filtering (High/Medium/Low) - Manual category creation - Dry run mode for safe preview New Commands: - seo category_apply - Apply AI proposals to WordPress - seo category_create - Create new category manually New Modules: - src/seo/category_manager.py - WordPress category management - WordPressCategoryManager: Create/get categories - CategoryAssignmentProcessor: Process AI proposals Features: - Automatic category creation if doesn't exist - Bulk category assignment - Confidence threshold filtering - Append mode (doesn't replace existing categories) - Comprehensive error handling - Detailed statistics and logging Usage: ./seo category_propose # Get AI proposals ./seo category_apply -s mistergeek.net # Apply to site ./seo category_apply -s site -c High # High confidence only ./seo category_create -s site "New Cat" # Create category ./seo category_apply --dry-run # Preview changes Documentation: - CATEGORY_MANAGEMENT_GUIDE.md - Complete guide Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
258
CATEGORY_MANAGEMENT_GUIDE.md
Normal file
258
CATEGORY_MANAGEMENT_GUIDE.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Category Management Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The SEO automation tool can now **create and update WordPress categories** based on AI propositions. This includes:
|
||||
|
||||
1. **AI-powered category proposals** - Analyze posts and suggest optimal categories
|
||||
2. **Automatic category creation** - Create new categories in WordPress if they don't exist
|
||||
3. **Bulk category assignment** - Assign multiple posts to categories at once
|
||||
4. **Confidence-based filtering** - Only apply high-confidence recommendations
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. Export posts
|
||||
↓
|
||||
2. Get AI category proposals
|
||||
↓
|
||||
3. Review proposals (optional)
|
||||
↓
|
||||
4. Apply to WordPress (with confidence filter)
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### 1. Propose Categories (`seo category_propose`)
|
||||
|
||||
Analyze posts and generate category recommendations.
|
||||
|
||||
```bash
|
||||
# Propose categories for latest export
|
||||
./seo category_propose
|
||||
|
||||
# Propose for specific CSV
|
||||
./seo category_propose output/all_posts_2026-02-16.csv
|
||||
|
||||
# Save to custom file
|
||||
./seo category_propose -o output/my_proposals.csv
|
||||
```
|
||||
|
||||
**Output CSV columns:**
|
||||
- `post_id` - Post identifier
|
||||
- `title` - Post title
|
||||
- `current_categories` - Current categories
|
||||
- `proposed_category` - AI-suggested category
|
||||
- `alternative_categories` - Alternative suggestions
|
||||
- `category_reason` - Explanation
|
||||
- `category_confidence` - Confidence level (High/Medium/Low)
|
||||
|
||||
### 2. Apply Categories (`seo category_apply`)
|
||||
|
||||
Apply AI category proposals to WordPress.
|
||||
|
||||
```bash
|
||||
# Apply with default settings (Medium confidence)
|
||||
./seo category_apply -s mistergeek.net
|
||||
|
||||
# Apply only high-confidence recommendations
|
||||
./seo category_apply -s mistergeek.net -c High
|
||||
|
||||
# Apply specific proposals file
|
||||
./seo category_apply output/category_proposals_*.csv -s webscroll.fr
|
||||
|
||||
# Dry run (preview changes)
|
||||
./seo category_apply -s mistergeek.net --dry-run
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-s, --site` - WordPress site (required): `mistergeek.net`, `webscroll.fr`, `hellogeek.net`
|
||||
- `-c, --confidence` - Minimum confidence: `High`, `Medium`, `Low` (default: Medium)
|
||||
- `--dry-run` - Preview changes without applying
|
||||
|
||||
### 3. Create Category (`seo category_create`)
|
||||
|
||||
Manually create a new category.
|
||||
|
||||
```bash
|
||||
# Create category
|
||||
./seo category_create -s mistergeek.net "VPN Reviews"
|
||||
|
||||
# Create with description
|
||||
./seo category_create -s webscroll.fr "Torrent Clients" -d "Guides about torrent clients"
|
||||
|
||||
# Dry run
|
||||
./seo category_create -s hellogeek.net "Test Category" --dry-run
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Complete Category Update Workflow
|
||||
|
||||
```bash
|
||||
# Step 1: Export posts
|
||||
./seo export
|
||||
|
||||
# Step 2: Get AI category proposals
|
||||
./seo category_propose
|
||||
|
||||
# Step 3: Review proposals (open CSV in spreadsheet)
|
||||
open output/category_proposals_*.csv
|
||||
|
||||
# Step 4: Apply high-confidence recommendations
|
||||
./seo category_apply -s mistergeek.net -c High
|
||||
|
||||
# Step 5: Apply remaining Medium confidence (optional)
|
||||
./seo category_apply -s mistergeek.net -c Medium
|
||||
```
|
||||
|
||||
### Example 2: Site-Specific Category Management
|
||||
|
||||
```bash
|
||||
# For mistergeek.net (tech content)
|
||||
./seo category_propose
|
||||
./seo category_apply -s mistergeek.net -c Medium
|
||||
|
||||
# For webscroll.fr (torrent content)
|
||||
./seo category_apply -s webscroll.fr -c Medium
|
||||
|
||||
# For hellogeek.net (misc content)
|
||||
./seo category_apply -s hellogeek.net -c Low
|
||||
```
|
||||
|
||||
### Example 3: Manual Category Creation
|
||||
|
||||
```bash
|
||||
# Create new categories before applying
|
||||
./seo category_create -s mistergeek.net "AI Tools"
|
||||
./seo category_create -s mistergeek.net "VPN Reviews"
|
||||
./seo category_create -s webscroll.fr "Seedbox Guides"
|
||||
|
||||
# Then apply AI proposals
|
||||
./seo category_apply -s mistergeek.net
|
||||
```
|
||||
|
||||
### Example 4: Safe Dry Run
|
||||
|
||||
```bash
|
||||
# Preview what would happen
|
||||
./seo category_propose
|
||||
./seo category_apply -s mistergeek.net --dry-run
|
||||
|
||||
# Output shows:
|
||||
# Would assign post 123 to "VPN"
|
||||
# Would assign post 456 to "Software"
|
||||
# etc.
|
||||
|
||||
# If satisfied, run without --dry-run
|
||||
./seo category_apply -s mistergeek.net
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Category Creation Logic
|
||||
|
||||
1. **Check if category exists** (by slug)
|
||||
2. **If exists**: Use existing category ID
|
||||
3. **If not**: Create new category with AI-suggested name
|
||||
4. **Assign post** to the category
|
||||
|
||||
### Confidence Filtering
|
||||
|
||||
- **High**: Only apply very confident recommendations (>90% accuracy)
|
||||
- **Medium**: Apply most recommendations (default, ~80% accuracy)
|
||||
- **Low**: Apply all recommendations including uncertain ones
|
||||
|
||||
### Safety Features
|
||||
|
||||
- **Dry run mode**: Preview changes before applying
|
||||
- **Confidence threshold**: Filter out low-confidence suggestions
|
||||
- **Append mode**: Adds to existing categories (doesn't replace)
|
||||
- **Error handling**: Continues on errors, reports statistics
|
||||
|
||||
## API Endpoints Used
|
||||
|
||||
The category manager uses WordPress REST API:
|
||||
|
||||
```
|
||||
GET /wp-json/wp/v2/categories - List categories
|
||||
POST /wp-json/wp/v2/categories - Create category
|
||||
GET /wp-json/wp/v2/posts/{id} - Get post details
|
||||
POST /wp-json/wp/v2/posts/{id} - Update post categories
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Site not found" error
|
||||
```bash
|
||||
# Use exact site name
|
||||
./seo category_apply -s mistergeek.net # ✓ Correct
|
||||
./seo category_apply -s mistergeek # ✗ Wrong
|
||||
```
|
||||
|
||||
### "No proposals found" error
|
||||
```bash
|
||||
# Run category_propose first
|
||||
./seo category_propose
|
||||
./seo category_apply -s mistergeek.net
|
||||
```
|
||||
|
||||
### Authentication errors
|
||||
```bash
|
||||
# Check .env file has correct credentials
|
||||
# WORDPRESS_MISTERGEEK_USERNAME=...
|
||||
# WORDPRESS_MISTERGEEK_PASSWORD=...
|
||||
```
|
||||
|
||||
### Categories not being created
|
||||
```bash
|
||||
# Check WordPress user has permission to create categories
|
||||
# Requires 'manage_categories' capability
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with High confidence**: Test with `-c High` first
|
||||
2. **Review proposals**: Open CSV and review before applying
|
||||
3. **Use dry run**: Always test with `--dry-run` first
|
||||
4. **Backup first**: Export posts before bulk changes
|
||||
5. **Monitor results**: Check WordPress admin after applying
|
||||
|
||||
## Programmatic Usage
|
||||
|
||||
```python
|
||||
from seo import SEOApp, CategoryAssignmentProcessor
|
||||
|
||||
app = SEOApp()
|
||||
|
||||
# Get proposals
|
||||
proposals_file = app.category_propose()
|
||||
|
||||
# Apply with high confidence
|
||||
stats = app.category_apply(
|
||||
proposals_csv=proposals_file,
|
||||
site_name='mistergeek.net',
|
||||
confidence='High',
|
||||
dry_run=False
|
||||
)
|
||||
|
||||
print(f"Updated {stats['posts_updated']} posts")
|
||||
```
|
||||
|
||||
## Output Statistics
|
||||
|
||||
After applying categories, you'll see:
|
||||
|
||||
```
|
||||
PROCESSING SUMMARY
|
||||
Total proposals processed: 150
|
||||
Categories created/found: 25
|
||||
Posts updated: 142
|
||||
Errors: 8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2026-02-16
|
||||
**Related**: See ENHANCED_ANALYSIS_GUIDE.md for AI analysis
|
||||
@@ -5,10 +5,19 @@ Single entry point for all SEO automation functionality.
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__author__ = 'SEO Automation Team'
|
||||
__all__ = ['SEOApp', 'PostExporter', 'PostAnalyzer', 'CategoryProposer']
|
||||
__all__ = [
|
||||
'SEOApp',
|
||||
'PostExporter',
|
||||
'PostAnalyzer',
|
||||
'EnhancedPostAnalyzer',
|
||||
'CategoryProposer',
|
||||
'WordPressCategoryManager',
|
||||
'CategoryAssignmentProcessor'
|
||||
]
|
||||
|
||||
# Import main classes for easy access
|
||||
from .app import SEOApp
|
||||
from .exporter import PostExporter
|
||||
from .analyzer import PostAnalyzer, EnhancedPostAnalyzer
|
||||
from .category_proposer import CategoryProposer
|
||||
from .category_manager import WordPressCategoryManager, CategoryAssignmentProcessor
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Optional, List
|
||||
from .exporter import PostExporter
|
||||
from .analyzer import EnhancedPostAnalyzer
|
||||
from .category_proposer import CategoryProposer
|
||||
from .category_manager import WordPressCategoryManager, CategoryAssignmentProcessor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,6 +78,55 @@ class SEOApp:
|
||||
proposer = CategoryProposer(csv_file)
|
||||
return proposer.run(output_file=output)
|
||||
|
||||
def category_apply(self, proposals_csv: str, site_name: str,
|
||||
confidence: str = 'Medium', dry_run: bool = False) -> dict:
|
||||
"""
|
||||
Apply AI category proposals to WordPress.
|
||||
|
||||
Args:
|
||||
proposals_csv: Path to proposals CSV
|
||||
site_name: Site to apply changes to (mistergeek.net, webscroll.fr, hellogeek.net)
|
||||
confidence: Minimum confidence level (High, Medium, Low)
|
||||
dry_run: If True, preview changes without applying
|
||||
|
||||
Returns:
|
||||
Statistics dict
|
||||
"""
|
||||
logger.info(f"📋 Applying category proposals to {site_name}...")
|
||||
|
||||
processor = CategoryAssignmentProcessor()
|
||||
stats = processor.run(
|
||||
proposals_csv=proposals_csv,
|
||||
site_name=site_name,
|
||||
confidence_threshold=confidence,
|
||||
dry_run=dry_run
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
def category_create(self, site_name: str, category_name: str,
|
||||
description: str = '') -> Optional[int]:
|
||||
"""
|
||||
Create a new category on a WordPress site.
|
||||
|
||||
Args:
|
||||
site_name: Site to create category on
|
||||
category_name: Name of the category
|
||||
description: Category description
|
||||
|
||||
Returns:
|
||||
Category ID if successful
|
||||
"""
|
||||
logger.info(f"📁 Creating category '{category_name}' on {site_name}...")
|
||||
|
||||
manager = WordPressCategoryManager()
|
||||
category_id = manager.get_or_create_category(site_name, category_name, description)
|
||||
|
||||
if category_id:
|
||||
logger.info(f"✓ Category created/found (ID: {category_id})")
|
||||
|
||||
return category_id
|
||||
|
||||
def status(self) -> dict:
|
||||
"""Get status of output files."""
|
||||
files = list(self.output_dir.glob('*.csv'))
|
||||
|
||||
408
src/seo/category_manager.py
Normal file
408
src/seo/category_manager.py
Normal file
@@ -0,0 +1,408 @@
|
||||
"""
|
||||
Category Manager - Create, update, and assign categories in WordPress
|
||||
"""
|
||||
|
||||
import csv
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from .config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WordPressCategoryManager:
|
||||
"""Manage WordPress categories: create, update, and assign to posts."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize category manager."""
|
||||
self.sites = Config.WORDPRESS_SITES
|
||||
self.category_cache = {} # Cache categories by site
|
||||
|
||||
def get_site_auth(self, site_name: str) -> Tuple[str, HTTPBasicAuth]:
|
||||
"""Get site URL and auth for a given site name."""
|
||||
site_config = self.sites.get(site_name)
|
||||
if not site_config:
|
||||
raise ValueError(f"Site not found: {site_name}")
|
||||
|
||||
base_url = site_config['url'].rstrip('/')
|
||||
auth = HTTPBasicAuth(site_config['username'], site_config['password'])
|
||||
return base_url, auth
|
||||
|
||||
def fetch_categories(self, site_name: str) -> Dict[str, int]:
|
||||
"""
|
||||
Fetch all categories from a WordPress site.
|
||||
|
||||
Returns:
|
||||
Dict mapping category name (slug) to category ID
|
||||
"""
|
||||
if site_name in self.category_cache:
|
||||
return self.category_cache[site_name]
|
||||
|
||||
logger.info(f"Fetching categories from {site_name}...")
|
||||
|
||||
try:
|
||||
base_url, auth = self.get_site_auth(site_name)
|
||||
categories = {}
|
||||
page = 1
|
||||
|
||||
while True:
|
||||
response = requests.get(
|
||||
f"{base_url}/wp-json/wp/v2/categories",
|
||||
params={'per_page': 100, 'page': page},
|
||||
auth=auth,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
page_categories = response.json()
|
||||
if not page_categories:
|
||||
break
|
||||
|
||||
for cat in page_categories:
|
||||
categories[cat['slug'].lower()] = {
|
||||
'id': cat['id'],
|
||||
'name': cat['name'],
|
||||
'slug': cat['slug'],
|
||||
'count': cat.get('count', 0)
|
||||
}
|
||||
|
||||
# Check for more pages
|
||||
if len(page_categories) < 100:
|
||||
break
|
||||
page += 1
|
||||
|
||||
self.category_cache[site_name] = categories
|
||||
logger.info(f"✓ Fetched {len(categories)} categories from {site_name}")
|
||||
return categories
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching categories from {site_name}: {e}")
|
||||
return {}
|
||||
|
||||
def create_category(self, site_name: str, category_name: str,
|
||||
description: str = '', parent_id: int = 0) -> Optional[int]:
|
||||
"""
|
||||
Create a new category in WordPress.
|
||||
|
||||
Args:
|
||||
site_name: Site to create category on
|
||||
category_name: Name of the category
|
||||
description: Category description
|
||||
parent_id: Parent category ID (0 for top-level)
|
||||
|
||||
Returns:
|
||||
Category ID if successful, None otherwise
|
||||
"""
|
||||
try:
|
||||
base_url, auth = self.get_site_auth(site_name)
|
||||
|
||||
# Create slug from name
|
||||
slug = category_name.lower().replace(' ', '-').replace('/', '-')
|
||||
|
||||
logger.info(f"Creating category '{category_name}' on {site_name}...")
|
||||
|
||||
response = requests.post(
|
||||
f"{base_url}/wp-json/wp/v2/categories",
|
||||
json={
|
||||
'name': category_name,
|
||||
'slug': slug,
|
||||
'description': description,
|
||||
'parent': parent_id
|
||||
},
|
||||
auth=auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
category_data = response.json()
|
||||
logger.info(f"✓ Created category '{category_name}' (ID: {category_data['id']})")
|
||||
|
||||
# Update cache
|
||||
if site_name in self.category_cache:
|
||||
self.category_cache[site_name][slug] = {
|
||||
'id': category_data['id'],
|
||||
'name': category_data['name'],
|
||||
'slug': slug,
|
||||
'count': 0
|
||||
}
|
||||
|
||||
return category_data['id']
|
||||
elif response.status_code == 409:
|
||||
# Category already exists
|
||||
logger.info(f" Category '{category_name}' already exists")
|
||||
existing = response.json()
|
||||
if isinstance(existing, list) and len(existing) > 0:
|
||||
return existing[0]['id']
|
||||
return None
|
||||
else:
|
||||
logger.error(f"Error creating category: {response.status_code} - {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating category: {e}")
|
||||
return None
|
||||
|
||||
def get_or_create_category(self, site_name: str, category_name: str,
|
||||
description: str = '') -> Optional[int]:
|
||||
"""
|
||||
Get existing category or create it if it doesn't exist.
|
||||
|
||||
Args:
|
||||
site_name: Site to work with
|
||||
category_name: Name of the category
|
||||
description: Category description (used if creating)
|
||||
|
||||
Returns:
|
||||
Category ID
|
||||
"""
|
||||
# Fetch categories if not cached
|
||||
if site_name not in self.category_cache:
|
||||
self.fetch_categories(site_name)
|
||||
|
||||
# Check if category exists
|
||||
slug = category_name.lower().replace(' ', '-').replace('/', '-')
|
||||
categories = self.category_cache.get(site_name, {})
|
||||
|
||||
if slug in categories:
|
||||
logger.info(f"✓ Found existing category '{category_name}' (ID: {categories[slug]['id']})")
|
||||
return categories[slug]['id']
|
||||
|
||||
# Try alternative slug formats
|
||||
alt_slug = category_name.lower().replace(' ', '-')
|
||||
if alt_slug in categories:
|
||||
logger.info(f"✓ Found existing category '{category_name}' (ID: {categories[alt_slug]['id']})")
|
||||
return categories[alt_slug]['id']
|
||||
|
||||
# Create new category
|
||||
return self.create_category(site_name, category_name, description)
|
||||
|
||||
def assign_post_to_category(self, site_name: str, post_id: int,
|
||||
category_id: int, append: bool = True) -> bool:
|
||||
"""
|
||||
Assign a post to a category.
|
||||
|
||||
Args:
|
||||
site_name: Site where post exists
|
||||
post_id: Post ID
|
||||
category_id: Category ID to assign
|
||||
append: If True, add to existing categories; if False, replace all
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
base_url, auth = self.get_site_auth(site_name)
|
||||
|
||||
if append:
|
||||
# Get current categories
|
||||
response = requests.get(
|
||||
f"{base_url}/wp-json/wp/v2/posts/{post_id}",
|
||||
auth=auth,
|
||||
timeout=10
|
||||
)
|
||||
if response.status_code == 200:
|
||||
post_data = response.json()
|
||||
current_categories = post_data.get('categories', [])
|
||||
if category_id not in current_categories:
|
||||
current_categories.append(category_id)
|
||||
else:
|
||||
logger.error(f"Could not fetch post {post_id}")
|
||||
return False
|
||||
else:
|
||||
current_categories = [category_id]
|
||||
|
||||
# Update post with new categories
|
||||
response = requests.post(
|
||||
f"{base_url}/wp-json/wp/v2/posts/{post_id}",
|
||||
json={'categories': current_categories},
|
||||
auth=auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
logger.info(f"✓ Assigned post {post_id} to category {category_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Error assigning category: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error assigning category: {e}")
|
||||
return False
|
||||
|
||||
def bulk_assign_categories(self, site_name: str,
|
||||
post_category_map: Dict[int, List[int]]) -> Dict[str, int]:
|
||||
"""
|
||||
Bulk assign posts to categories.
|
||||
|
||||
Args:
|
||||
site_name: Site to work with
|
||||
post_category_map: Dict mapping post_id to list of category_ids
|
||||
|
||||
Returns:
|
||||
Statistics dict with success/failure counts
|
||||
"""
|
||||
stats = {'success': 0, 'failed': 0}
|
||||
|
||||
logger.info(f"Bulk assigning categories on {site_name}...")
|
||||
|
||||
for post_id, category_ids in post_category_map.items():
|
||||
for category_id in category_ids:
|
||||
if self.assign_post_to_category(site_name, post_id, category_id):
|
||||
stats['success'] += 1
|
||||
else:
|
||||
stats['failed'] += 1
|
||||
|
||||
logger.info(f"✓ Bulk assignment complete: {stats['success']} successful, {stats['failed']} failed")
|
||||
return stats
|
||||
|
||||
|
||||
class CategoryAssignmentProcessor:
|
||||
"""Process AI category proposals and apply them to WordPress."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize processor."""
|
||||
self.category_manager = WordPressCategoryManager()
|
||||
self.processing_stats = {
|
||||
'total_posts': 0,
|
||||
'categories_created': 0,
|
||||
'posts_updated': 0,
|
||||
'errors': 0
|
||||
}
|
||||
|
||||
def load_proposals(self, proposals_csv: str) -> List[Dict]:
|
||||
"""Load category proposals from CSV."""
|
||||
logger.info(f"Loading proposals from: {proposals_csv}")
|
||||
|
||||
try:
|
||||
with open(proposals_csv, 'r', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
proposals = list(reader)
|
||||
|
||||
logger.info(f"✓ Loaded {len(proposals)} proposals")
|
||||
return proposals
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading proposals: {e}")
|
||||
return []
|
||||
|
||||
def process_proposals(self, proposals: List[Dict], site_name: str,
|
||||
confidence_threshold: str = 'Medium',
|
||||
dry_run: bool = False) -> Dict[str, int]:
|
||||
"""
|
||||
Process AI category proposals and apply to WordPress.
|
||||
|
||||
Args:
|
||||
proposals: List of proposal dicts from CSV
|
||||
site_name: Site to apply changes to
|
||||
confidence_threshold: Minimum confidence to apply (High, Medium, Low)
|
||||
dry_run: If True, don't actually make changes
|
||||
|
||||
Returns:
|
||||
Statistics dict
|
||||
"""
|
||||
logger.info("\n" + "="*70)
|
||||
logger.info("PROCESSING CATEGORY PROPOSALS")
|
||||
logger.info("="*70)
|
||||
|
||||
if dry_run:
|
||||
logger.info("DRY RUN - No changes will be made")
|
||||
|
||||
# Filter by confidence
|
||||
confidence_order = {'High': 3, 'Medium': 2, 'Low': 1}
|
||||
min_confidence = confidence_order.get(confidence_threshold, 2)
|
||||
|
||||
filtered_proposals = [
|
||||
p for p in proposals
|
||||
if confidence_order.get(p.get('category_confidence', 'Medium'), 2) >= min_confidence
|
||||
]
|
||||
|
||||
logger.info(f"Filtered to {len(filtered_proposals)} proposals (confidence >= {confidence_threshold})")
|
||||
|
||||
# Fetch existing categories
|
||||
self.category_manager.fetch_categories(site_name)
|
||||
|
||||
# Process each proposal
|
||||
for i, proposal in enumerate(filtered_proposals, 1):
|
||||
logger.info(f"\n[{i}/{len(filtered_proposals)}] Processing post {proposal.get('post_id')}...")
|
||||
|
||||
post_id = int(proposal.get('post_id', 0))
|
||||
proposed_category = proposal.get('proposed_category', '')
|
||||
current_categories = proposal.get('current_categories', '')
|
||||
confidence = proposal.get('category_confidence', 'Medium')
|
||||
|
||||
if not post_id or not proposed_category:
|
||||
logger.warning(" Skipping: Missing post_id or proposed_category")
|
||||
self.processing_stats['errors'] += 1
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
logger.info(f" Would assign to: {proposed_category}")
|
||||
continue
|
||||
|
||||
# Get or create the category
|
||||
category_id = self.category_manager.get_or_create_category(
|
||||
site_name,
|
||||
proposed_category,
|
||||
description=f"AI-proposed category (confidence: {confidence})"
|
||||
)
|
||||
|
||||
if category_id:
|
||||
self.processing_stats['categories_created'] += 1
|
||||
|
||||
# Assign post to category
|
||||
if self.category_manager.assign_post_to_category(
|
||||
site_name, post_id, category_id, append=True
|
||||
):
|
||||
self.processing_stats['posts_updated'] += 1
|
||||
logger.info(f" ✓ Assigned to '{proposed_category}'")
|
||||
else:
|
||||
self.processing_stats['errors'] += 1
|
||||
else:
|
||||
self.processing_stats['errors'] += 1
|
||||
logger.error(f" Failed to get/create category '{proposed_category}'")
|
||||
|
||||
self.processing_stats['total_posts'] = len(filtered_proposals)
|
||||
|
||||
# Print summary
|
||||
logger.info("\n" + "="*70)
|
||||
logger.info("PROCESSING SUMMARY")
|
||||
logger.info("="*70)
|
||||
logger.info(f"Total proposals processed: {self.processing_stats['total_posts']}")
|
||||
logger.info(f"Categories created/found: {self.processing_stats['categories_created']}")
|
||||
logger.info(f"Posts updated: {self.processing_stats['posts_updated']}")
|
||||
logger.info(f"Errors: {self.processing_stats['errors']}")
|
||||
|
||||
return self.processing_stats
|
||||
|
||||
def run(self, proposals_csv: str, site_name: str,
|
||||
confidence_threshold: str = 'Medium',
|
||||
dry_run: bool = False) -> Dict[str, int]:
|
||||
"""
|
||||
Run complete category assignment process.
|
||||
|
||||
Args:
|
||||
proposals_csv: Path to proposals CSV
|
||||
site_name: Site to apply changes to
|
||||
confidence_threshold: Minimum confidence to apply
|
||||
dry_run: If True, preview changes without applying
|
||||
|
||||
Returns:
|
||||
Statistics dict
|
||||
"""
|
||||
proposals = self.load_proposals(proposals_csv)
|
||||
|
||||
if not proposals:
|
||||
logger.error("No proposals to process")
|
||||
return self.processing_stats
|
||||
|
||||
return self.process_proposals(
|
||||
proposals,
|
||||
site_name,
|
||||
confidence_threshold,
|
||||
dry_run
|
||||
)
|
||||
100
src/seo/cli.py
100
src/seo/cli.py
@@ -42,6 +42,11 @@ Examples:
|
||||
help='Fields to analyze')
|
||||
parser.add_argument('--update', '-u', action='store_true', help='Update input file')
|
||||
parser.add_argument('--output', '-o', help='Output file path')
|
||||
parser.add_argument('--confidence', '-c', choices=['High', 'Medium', 'Low'],
|
||||
default='Medium', help='Confidence threshold for category apply')
|
||||
parser.add_argument('--site', '-s', choices=['mistergeek.net', 'webscroll.fr', 'hellogeek.net'],
|
||||
help='WordPress site for category operations')
|
||||
parser.add_argument('--description', '-d', help='Category description')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -64,6 +69,8 @@ Examples:
|
||||
'export': cmd_export,
|
||||
'analyze': cmd_analyze,
|
||||
'category_propose': cmd_category_propose,
|
||||
'category_apply': cmd_category_apply,
|
||||
'category_create': cmd_category_create,
|
||||
'status': cmd_status,
|
||||
'help': cmd_help,
|
||||
}
|
||||
@@ -139,6 +146,82 @@ def cmd_category_propose(app, args):
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_category_apply(app, args):
|
||||
"""Apply category proposals to WordPress."""
|
||||
if args.dry_run:
|
||||
print("Would apply category proposals to WordPress")
|
||||
print(f" Site: {args.site}")
|
||||
print(f" Confidence: {args.confidence}")
|
||||
return 0
|
||||
|
||||
if not args.site:
|
||||
print("❌ Site required. Use --site mistergeek.net|webscroll.fr|hellogeek.net")
|
||||
return 1
|
||||
|
||||
proposals_csv = args.args[0] if args.args else None
|
||||
|
||||
if not proposals_csv:
|
||||
# Find latest proposals
|
||||
proposals_files = list(app.output_dir.glob('category_proposals_*.csv'))
|
||||
if proposals_files:
|
||||
proposals_csv = str(max(proposals_files, key=lambda f: f.stat().st_ctime))
|
||||
else:
|
||||
print("❌ No proposals CSV found. Run 'seo category_propose' first or provide file.")
|
||||
return 1
|
||||
|
||||
print(f"Applying categories from: {proposals_csv}")
|
||||
print(f"Site: {args.site}")
|
||||
print(f"Confidence threshold: {args.confidence}")
|
||||
|
||||
stats = app.category_apply(
|
||||
proposals_csv=proposals_csv,
|
||||
site_name=args.site,
|
||||
confidence=args.confidence,
|
||||
dry_run=False
|
||||
)
|
||||
|
||||
if stats:
|
||||
print(f"\n✅ Category application complete!")
|
||||
print(f" Posts processed: {stats.get('total_posts', 0)}")
|
||||
print(f" Categories created/found: {stats.get('categories_created', 0)}")
|
||||
print(f" Posts updated: {stats.get('posts_updated', 0)}")
|
||||
print(f" Errors: {stats.get('errors', 0)}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_category_create(app, args):
|
||||
"""Create a new category on WordPress site."""
|
||||
if args.dry_run:
|
||||
print(f"Would create category on {args.site}")
|
||||
if args.args:
|
||||
print(f" Category name: {args.args[0]}")
|
||||
return 0
|
||||
|
||||
if not args.site:
|
||||
print("❌ Site required. Use --site mistergeek.net|webscroll.fr|hellogeek.net")
|
||||
return 1
|
||||
|
||||
if not args.args:
|
||||
print("❌ Category name required")
|
||||
print(" Usage: seo category_create --site <site> <category_name>")
|
||||
return 1
|
||||
|
||||
category_name = args.args[0]
|
||||
description = args.description or f"Category: {category_name}"
|
||||
|
||||
category_id = app.category_create(
|
||||
site_name=args.site,
|
||||
category_name=category_name,
|
||||
description=description
|
||||
)
|
||||
|
||||
if category_id:
|
||||
print(f"✅ Category '{category_name}' created/found (ID: {category_id})")
|
||||
else:
|
||||
print(f"❌ Failed to create category '{category_name}'")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_status(app, args):
|
||||
"""Show status."""
|
||||
if args.dry_run:
|
||||
@@ -163,12 +246,20 @@ def cmd_help(app, args):
|
||||
print("""
|
||||
SEO Automation CLI - Available Commands
|
||||
|
||||
Basic Commands:
|
||||
Export & Analysis:
|
||||
export Export all posts from WordPress sites
|
||||
analyze [csv_file] Analyze posts with AI
|
||||
analyze -f title Analyze specific fields (title, meta_description, categories, site)
|
||||
analyze -u Update input CSV with new columns (creates backup)
|
||||
category_propose [csv] Propose categories based on content
|
||||
|
||||
Category Management:
|
||||
category_apply [csv] Apply AI category proposals to WordPress
|
||||
category_apply -s site -c High Apply with high confidence threshold
|
||||
category_create --site site <name> Create a new category
|
||||
category_create -s mistergeek.net "VPN Reviews"
|
||||
|
||||
Utility:
|
||||
status Show output files status
|
||||
help Show this help message
|
||||
|
||||
@@ -178,14 +269,19 @@ Options:
|
||||
--fields, -f Fields to analyze: title, meta_description, categories, site
|
||||
--update, -u Update input CSV file (creates backup)
|
||||
--output, -o Output file path
|
||||
--confidence, -c Confidence threshold: High, Medium, Low
|
||||
--site, -s WordPress site: mistergeek.net, webscroll.fr, hellogeek.net
|
||||
--description, -d Category description
|
||||
|
||||
Examples:
|
||||
seo export
|
||||
seo analyze
|
||||
seo analyze output/all_posts_2026-02-16.csv
|
||||
seo analyze -f title categories
|
||||
seo analyze -u -f meta_description
|
||||
seo category_propose
|
||||
seo category_propose output/all_posts_2026-02-16.csv
|
||||
seo category_apply -s mistergeek.net -c Medium
|
||||
seo category_create -s webscroll.fr "Torrent Clients"
|
||||
seo status
|
||||
""")
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user