Major refactoring to create a clean, integrated CLI application: ### New Features: - Unified CLI executable (./seo) with simple command structure - All commands accept optional CSV file arguments - Auto-detection of latest files when no arguments provided - Simplified output directory structure (output/ instead of output/reports/) - Cleaner export filename format (all_posts_YYYY-MM-DD.csv) ### Commands: - export: Export all posts from WordPress sites - analyze [csv]: Analyze posts with AI (optional CSV input) - recategorize [csv]: Recategorize posts with AI - seo_check: Check SEO quality - categories: Manage categories across sites - approve [files]: Review and approve recommendations - full_pipeline: Run complete workflow - analytics, gaps, opportunities, report, status ### Changes: - Moved all scripts to scripts/ directory - Created config.yaml for configuration - Updated all scripts to use output/ directory - Deprecated old seo-cli.py in favor of new ./seo - Added AGENTS.md and CHANGELOG.md documentation - Consolidated README.md with updated usage ### Technical: - Added PyYAML dependency - Removed hardcoded configuration values - All scripts now properly integrated - Better error handling and user feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
352 lines
14 KiB
Python
352 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
User Approval Mechanism for SEO Recommendations
|
|
Allows users to review and approve recommendations from CSV files.
|
|
"""
|
|
|
|
import csv
|
|
import json
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
from datetime import datetime
|
|
from config import Config
|
|
|
|
# Setup logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class UserApprovalSystem:
|
|
"""System for reviewing and approving SEO recommendations."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the approval system."""
|
|
self.output_dir = Path(__file__).parent.parent / 'output'
|
|
self.approved_recommendations = []
|
|
self.rejected_recommendations = []
|
|
self.pending_recommendations = []
|
|
|
|
def load_recommendations_from_csv(self, csv_file: str) -> List[Dict]:
|
|
"""Load recommendations from CSV file."""
|
|
recommendations = []
|
|
|
|
if not Path(csv_file).exists():
|
|
logger.error(f"CSV file not found: {csv_file}")
|
|
return recommendations
|
|
|
|
try:
|
|
with open(csv_file, 'r', encoding='utf-8') as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
recommendations.append(dict(row))
|
|
|
|
logger.info(f"Loaded {len(recommendations)} recommendations from {csv_file}")
|
|
return recommendations
|
|
except Exception as e:
|
|
logger.error(f"Error loading CSV: {e}")
|
|
return recommendations
|
|
|
|
def display_recommendation(self, recommendation: Dict, index: int, total: int):
|
|
"""Display a single recommendation for user review."""
|
|
print(f"\n{'='*80}")
|
|
print(f"RECOMMENDATION {index}/{total}")
|
|
print(f"{'='*80}")
|
|
|
|
# Display different fields depending on the type of recommendation
|
|
if 'post_title' in recommendation:
|
|
print(f"Post Title: {recommendation.get('post_title', 'N/A')}")
|
|
print(f"Post ID: {recommendation.get('post_id', 'N/A')}")
|
|
print(f"Site: {recommendation.get('site', 'N/A')}")
|
|
print(f"Current Categories: {recommendation.get('current_categories', 'N/A')}")
|
|
print(f"Proposed Category: {recommendation.get('proposed_category', 'N/A')}")
|
|
print(f"Proposed Site: {recommendation.get('proposed_site', 'N/A')}")
|
|
print(f"Reason: {recommendation.get('reason', 'N/A')}")
|
|
print(f"Confidence: {recommendation.get('confidence', 'N/A')}")
|
|
print(f"Content Preview: {recommendation.get('content_preview', 'N/A')[:100]}...")
|
|
elif 'title' in recommendation:
|
|
print(f"Post Title: {recommendation.get('title', 'N/A')}")
|
|
print(f"Post ID: {recommendation.get('post_id', 'N/A')}")
|
|
print(f"Site: {recommendation.get('site', 'N/A')}")
|
|
print(f"Decision: {recommendation.get('decision', 'N/A')}")
|
|
print(f"Recommended Category: {recommendation.get('recommended_category', 'N/A')}")
|
|
print(f"Reason: {recommendation.get('reason', 'N/A')}")
|
|
print(f"Priority: {recommendation.get('priority', 'N/A')}")
|
|
print(f"AI Notes: {recommendation.get('ai_notes', 'N/A')}")
|
|
else:
|
|
# Generic display for other types of recommendations
|
|
for key, value in recommendation.items():
|
|
print(f"{key.replace('_', ' ').title()}: {value}")
|
|
|
|
def get_user_choice(self) -> str:
|
|
"""Get user's approval choice."""
|
|
while True:
|
|
print(f"\nOptions:")
|
|
print(f" 'y' or 'yes' - Approve this recommendation")
|
|
print(f" 'n' or 'no' - Reject this recommendation")
|
|
print(f" 's' or 'skip' - Skip this recommendation for later review")
|
|
print(f" 'q' or 'quit' - Quit and save current progress")
|
|
|
|
choice = input(f"\nEnter your choice: ").strip().lower()
|
|
|
|
if choice in ['y', 'yes']:
|
|
return 'approved'
|
|
elif choice in ['n', 'no']:
|
|
return 'rejected'
|
|
elif choice in ['s', 'skip']:
|
|
return 'pending'
|
|
elif choice in ['q', 'quit']:
|
|
return 'quit'
|
|
else:
|
|
print("Invalid choice. Please enter 'y', 'n', 's', or 'q'.")
|
|
|
|
def review_recommendations(self, recommendations: List[Dict], title: str = "Recommendations"):
|
|
"""Review recommendations with user interaction."""
|
|
print(f"\n{'='*80}")
|
|
print(f"REVIEWING {title.upper()}")
|
|
print(f"Total recommendations to review: {len(recommendations)}")
|
|
print(f"{'='*80}")
|
|
|
|
for i, recommendation in enumerate(recommendations, 1):
|
|
self.display_recommendation(recommendation, i, len(recommendations))
|
|
|
|
choice = self.get_user_choice()
|
|
|
|
if choice == 'quit':
|
|
logger.info("User chose to quit. Saving progress...")
|
|
break
|
|
elif choice == 'approved':
|
|
recommendation['status'] = 'approved'
|
|
self.approved_recommendations.append(recommendation)
|
|
logger.info(f"Approved recommendation {i}")
|
|
elif choice == 'rejected':
|
|
recommendation['status'] = 'rejected'
|
|
self.rejected_recommendations.append(recommendation)
|
|
logger.info(f"Rejected recommendation {i}")
|
|
elif choice == 'pending':
|
|
recommendation['status'] = 'pending_review'
|
|
self.pending_recommendations.append(recommendation)
|
|
logger.info(f"Skipped recommendation {i} for later review")
|
|
|
|
def export_approved_recommendations(self, filename_suffix: str = "") -> str:
|
|
"""Export approved recommendations to CSV."""
|
|
if not self.approved_recommendations:
|
|
logger.info("No approved recommendations to export")
|
|
return ""
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
filename = f"approved_recommendations_{timestamp}{filename_suffix}.csv"
|
|
csv_file = self.output_dir / filename
|
|
|
|
# Get all unique fieldnames from recommendations
|
|
fieldnames = set()
|
|
for rec in self.approved_recommendations:
|
|
fieldnames.update(rec.keys())
|
|
fieldnames = sorted(list(fieldnames))
|
|
|
|
with open(csv_file, 'w', newline='', encoding='utf-8') as f:
|
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
writer.writerows(self.approved_recommendations)
|
|
|
|
logger.info(f"Exported {len(self.approved_recommendations)} approved recommendations to: {csv_file}")
|
|
return str(csv_file)
|
|
|
|
def export_rejected_recommendations(self, filename_suffix: str = "") -> str:
|
|
"""Export rejected recommendations to CSV."""
|
|
if not self.rejected_recommendations:
|
|
logger.info("No rejected recommendations to export")
|
|
return ""
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
filename = f"rejected_recommendations_{timestamp}{filename_suffix}.csv"
|
|
csv_file = self.output_dir / filename
|
|
|
|
# Get all unique fieldnames from recommendations
|
|
fieldnames = set()
|
|
for rec in self.rejected_recommendations:
|
|
fieldnames.update(rec.keys())
|
|
fieldnames = sorted(list(fieldnames))
|
|
|
|
with open(csv_file, 'w', newline='', encoding='utf-8') as f:
|
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
writer.writerows(self.rejected_recommendations)
|
|
|
|
logger.info(f"Exported {len(self.rejected_recommendations)} rejected recommendations to: {csv_file}")
|
|
return str(csv_file)
|
|
|
|
def export_pending_recommendations(self, filename_suffix: str = "") -> str:
|
|
"""Export pending recommendations to CSV."""
|
|
if not self.pending_recommendations:
|
|
logger.info("No pending recommendations to export")
|
|
return ""
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
filename = f"pending_recommendations_{timestamp}{filename_suffix}.csv"
|
|
csv_file = self.output_dir / filename
|
|
|
|
# Get all unique fieldnames from recommendations
|
|
fieldnames = set()
|
|
for rec in self.pending_recommendations:
|
|
fieldnames.update(rec.keys())
|
|
fieldnames = sorted(list(fieldnames))
|
|
|
|
with open(csv_file, 'w', newline='', encoding='utf-8') as f:
|
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
writer.writerows(self.pending_recommendations)
|
|
|
|
logger.info(f"Exported {len(self.pending_recommendations)} pending recommendations to: {csv_file}")
|
|
return str(csv_file)
|
|
|
|
def run_interactive_approval(self, csv_files: List[str]):
|
|
"""Run interactive approval process for multiple CSV files."""
|
|
logger.info("="*70)
|
|
logger.info("USER APPROVAL SYSTEM FOR SEO RECOMMENDATIONS")
|
|
logger.info("="*70)
|
|
|
|
for csv_file in csv_files:
|
|
logger.info(f"\nLoading recommendations from: {csv_file}")
|
|
recommendations = self.load_recommendations_from_csv(csv_file)
|
|
|
|
if not recommendations:
|
|
logger.warning(f"No recommendations found in {csv_file}, skipping...")
|
|
continue
|
|
|
|
# Get the filename without path for the title
|
|
filename = Path(csv_file).stem
|
|
self.review_recommendations(recommendations, title=filename)
|
|
|
|
# Export results
|
|
logger.info("\n" + "="*70)
|
|
logger.info("EXPORTING RESULTS")
|
|
logger.info("="*70)
|
|
|
|
approved_file = self.export_approved_recommendations()
|
|
rejected_file = self.export_rejected_recommendations()
|
|
pending_file = self.export_pending_recommendations()
|
|
|
|
# Summary
|
|
logger.info(f"\n{'─'*70}")
|
|
logger.info("APPROVAL SUMMARY:")
|
|
logger.info(f" Approved: {len(self.approved_recommendations)}")
|
|
logger.info(f" Rejected: {len(self.rejected_recommendations)}")
|
|
logger.info(f" Pending: {len(self.pending_recommendations)}")
|
|
logger.info(f"{'─'*70}")
|
|
|
|
if approved_file:
|
|
logger.info(f"\nApproved recommendations saved to: {approved_file}")
|
|
if rejected_file:
|
|
logger.info(f"Rejected recommendations saved to: {rejected_file}")
|
|
if pending_file:
|
|
logger.info(f"Pending recommendations saved to: {pending_file}")
|
|
|
|
logger.info(f"\n✓ Approval process complete!")
|
|
|
|
def run_auto_approval(self, csv_files: List[str], auto_approve_threshold: float = 0.8):
|
|
"""Auto-approve recommendations based on confidence threshold."""
|
|
logger.info("="*70)
|
|
logger.info("AUTO APPROVAL SYSTEM FOR SEO RECOMMENDATIONS")
|
|
logger.info("="*70)
|
|
logger.info(f"Auto-approval threshold: {auto_approve_threshold}")
|
|
|
|
all_recommendations = []
|
|
for csv_file in csv_files:
|
|
logger.info(f"\nLoading recommendations from: {csv_file}")
|
|
recommendations = self.load_recommendations_from_csv(csv_file)
|
|
all_recommendations.extend(recommendations)
|
|
|
|
approved_count = 0
|
|
rejected_count = 0
|
|
|
|
for rec in all_recommendations:
|
|
# Check if there's a confidence field and if it meets the threshold
|
|
confidence_str = rec.get('confidence', 'Low').lower()
|
|
confidence_value = 0.0
|
|
|
|
if confidence_str == 'high':
|
|
confidence_value = 0.9
|
|
elif confidence_str == 'medium':
|
|
confidence_value = 0.6
|
|
elif confidence_str == 'low':
|
|
confidence_value = 0.3
|
|
else:
|
|
# Try to parse as numeric value if possible
|
|
try:
|
|
confidence_value = float(confidence_str)
|
|
except ValueError:
|
|
confidence_value = 0.3 # Default to low
|
|
|
|
if confidence_value >= auto_approve_threshold:
|
|
rec['status'] = 'auto_approved'
|
|
self.approved_recommendations.append(rec)
|
|
approved_count += 1
|
|
else:
|
|
rec['status'] = 'auto_rejected'
|
|
self.rejected_recommendations.append(rec)
|
|
rejected_count += 1
|
|
|
|
# Export results
|
|
logger.info("\n" + "="*70)
|
|
logger.info("EXPORTING AUTO-APPROVAL RESULTS")
|
|
logger.info("="*70)
|
|
|
|
approved_file = self.export_approved_recommendations("_auto")
|
|
rejected_file = self.export_rejected_recommendations("_auto")
|
|
|
|
# Summary
|
|
logger.info(f"\n{'─'*70}")
|
|
logger.info("AUTO APPROVAL SUMMARY:")
|
|
logger.info(f" Auto-approved: {approved_count}")
|
|
logger.info(f" Auto-rejected: {rejected_count}")
|
|
logger.info(f"{'─'*70}")
|
|
|
|
if approved_file:
|
|
logger.info(f"\nAuto-approved recommendations saved to: {approved_file}")
|
|
if rejected_file:
|
|
logger.info(f"Auto-rejected recommendations saved to: {rejected_file}")
|
|
|
|
logger.info(f"\n✓ Auto-approval process complete!")
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description='Review and approve SEO recommendations'
|
|
)
|
|
parser.add_argument(
|
|
'csv_files',
|
|
nargs='+',
|
|
help='CSV files containing recommendations to review'
|
|
)
|
|
parser.add_argument(
|
|
'--auto',
|
|
action='store_true',
|
|
help='Run auto-approval mode instead of interactive mode'
|
|
)
|
|
parser.add_argument(
|
|
'--threshold',
|
|
type=float,
|
|
default=0.8,
|
|
help='Confidence threshold for auto-approval (default: 0.8)'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
approval_system = UserApprovalSystem()
|
|
|
|
if args.auto:
|
|
approval_system.run_auto_approval(args.csv_files, args.threshold)
|
|
else:
|
|
approval_system.run_interactive_approval(args.csv_files)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |