Files
eve-forum-moderator/src/eve_mod/cli.py
Ben 6dd4e3b6db refactor: compact report format with 85% confidence threshold
- Update report format to match Gemini-style table output
- Add confidence threshold (85% default) to filter noise
- Add --show-all flag to display all violations
- Combine quote and explanation in single Description column
- Much cleaner output: 12 high-confidence vs 51 total violations
2026-01-06 04:15:46 +00:00

267 lines
8.1 KiB
Python

"""CLI entrypoint for EVE Forum Moderator."""
import asyncio
from pathlib import Path
import click
from dotenv import load_dotenv
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn
from .analyzer import Analyzer, AnalyzerError
from .auth import AuthError, load_cookies
from .discourse import DiscourseClient, DiscourseError, UserNotFoundError
from .report import ReportGenerator
console = Console()
@click.group()
@click.version_option(version="0.1.0")
def cli() -> None:
"""EVE Forum Moderator - AI-assisted forum moderation tool."""
pass
@cli.command()
@click.argument("username")
@click.option(
"--days",
"-d",
default=30,
help="Number of days to analyze (default: 30)",
)
@click.option(
"--cookies",
"-c",
type=click.Path(exists=False),
default="cookies.env",
help="Path to cookies file (default: cookies.env)",
)
@click.option(
"--output",
"-o",
type=click.Path(),
default=None,
help="Custom output path for markdown report",
)
@click.option(
"--verbose",
"-v",
is_flag=True,
help="Show verbose output including detailed analysis",
)
@click.option(
"--max-posts",
default=200,
help="Maximum number of posts to fetch (default: 200)",
)
@click.option(
"--enrich/--no-enrich",
default=True,
help="Fetch full post content (slower but more accurate)",
)
@click.option(
"--debug",
is_flag=True,
help="Show debug output including sample post content",
)
@click.option(
"--show-all",
is_flag=True,
help="Show all violations regardless of confidence (default: 85%+ only)",
)
def review(
username: str,
days: int,
cookies: str,
output: str | None,
verbose: bool,
max_posts: int,
enrich: bool,
debug: bool,
show_all: bool,
) -> None:
"""
Review a user's forum posts for potential rule violations.
USERNAME is the EVE Forum username to review.
"""
# Load environment variables (for LLM_PROXY_KEY)
load_dotenv()
try:
# Load cookies
with console.status("[bold blue]Loading authentication cookies..."):
cookie_dict = load_cookies(Path(cookies))
console.print(f"[green]Loaded {len(cookie_dict)} cookies[/green]")
# Run async operations
asyncio.run(
_review_user(
username=username,
days=days,
cookies=cookie_dict,
output=output,
verbose=verbose,
max_posts=max_posts,
enrich=enrich,
debug=debug,
show_all=show_all,
)
)
except AuthError as e:
console.print(f"[red]Authentication Error:[/red] {e}")
raise SystemExit(1)
except AnalyzerError as e:
console.print(f"[red]Analysis Error:[/red] {e}")
raise SystemExit(1)
except Exception as e:
console.print(f"[red]Unexpected Error:[/red] {e}")
if verbose:
console.print_exception()
raise SystemExit(1)
async def _review_user(
username: str,
days: int,
cookies: dict[str, str],
output: str | None,
verbose: bool,
max_posts: int,
enrich: bool,
debug: bool = False,
show_all: bool = False,
) -> None:
"""Async implementation of user review."""
try:
async with DiscourseClient(cookies) as client:
# Fetch user profile
with console.status(f"[bold blue]Fetching profile for {username}..."):
try:
user = await client.get_user(username)
console.print(
f"[green]Found user:[/green] {user.username} "
f"(Trust Level {user.trust_level}, {user.post_count} posts)"
)
except UserNotFoundError:
console.print(f"[red]User not found:[/red] {username}")
raise SystemExit(1)
# Fetch posts (using combined method to include hidden posts)
with console.status(f"[bold blue]Fetching posts from last {days} days..."):
posts = await client.get_user_posts_combined(
username=username,
days=days,
max_posts=max_posts,
)
console.print(
f"[green]Found {len(posts)} posts (including hidden)[/green]"
)
if not posts:
console.print(
"[yellow]No posts found in the specified time period.[/yellow]"
)
return
# Enrich posts with full content
if enrich:
with console.status("[bold blue]Fetching full post content..."):
posts = await client.enrich_posts(posts)
console.print("[green]Enriched posts with full content[/green]")
# Debug: show sample post content
if debug and posts:
console.print("\n[bold yellow]DEBUG: Sample post content[/bold yellow]")
for i, post in enumerate(posts[:3]): # Show first 3 posts
console.print(f"\n[cyan]--- Post {i + 1} ---[/cyan]")
console.print(f"[dim]Topic:[/dim] {post.topic_title}")
console.print(f"[dim]URL:[/dim] {post.url}")
console.print(f"[dim]Date:[/dim] {post.created_at}")
console.print(
f"[dim]Raw length:[/dim] {len(post.content_raw)} chars"
)
console.print(
f"[dim]Text length:[/dim] {len(post.content_text)} chars"
)
content_preview = post.content_text[:500]
if len(post.content_text) > 500:
content_preview += "..."
console.print(f"[dim]Content:[/dim]\n{content_preview}")
console.print("\n")
except DiscourseError as e:
console.print(f"[red]Forum Error:[/red] {e}")
raise SystemExit(1)
# Analyze posts
console.print("[bold blue]Analyzing posts for rule violations...[/bold blue]")
try:
analyzer = Analyzer()
except AnalyzerError as e:
console.print(f"[red]Analyzer Error:[/red] {e}")
console.print(
"[dim]Hint: Set LLM_PROXY_KEY environment variable or add it to .env[/dim]"
)
raise SystemExit(1)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("Analyzing...", total=len(posts))
def update_progress(current: int, total: int) -> None:
progress.update(task, completed=current)
analyses = analyzer.analyze_all(
posts=posts,
progress_callback=update_progress,
)
# Generate report
confidence_threshold = 0.0 if show_all else 0.85
report_gen = ReportGenerator(confidence_threshold=confidence_threshold)
# Print to terminal
report_gen.print_summary(user, analyses, days)
if verbose:
report_gen.print_detailed(analyses)
# Save to file
if output:
filepath = Path(output)
filepath.parent.mkdir(parents=True, exist_ok=True)
filepath.write_text(report_gen.generate_markdown(user, analyses, days))
else:
filepath = report_gen.save_report(user, analyses, days)
console.print(f"\n[green]Report saved to:[/green] {filepath}")
@cli.command()
def rules() -> None:
"""Display all forum moderation rules."""
from .rules import ALL_RULES, RuleCategory
console.print("\n[bold]EVE Online Forum Moderation Rules[/bold]\n")
current_category = None
for rule in ALL_RULES:
if rule.category != current_category:
current_category = rule.category
console.print(f"\n[bold cyan]{current_category.value.upper()}[/bold cyan]")
console.print(f" [yellow]{rule.id}[/yellow] - [bold]{rule.name}[/bold]")
console.print(f" [dim]{rule.description[:80]}...[/dim]")
if __name__ == "__main__":
cli()