Files
athena-oracle/oracle/variants.py
T
Epictetus 07c5f9a5c2 Sprint 0+1: Package restructure, source tiers, verdicts, multi-variant editions
- New oracle/ package (11 modules) with unified CLI (python -m oracle)
- Source tiers: Tier 1 (arxiv/github/hf), Tier 2 (rss/hn), Tier 3 (reddit)
- Composite verdicts: PUBLISH/WATCH/ARCHIVE/DROP based on signal score + age
- Content-hash dedup: SHA-256[:16] normalized, atomic at insert time
- Multi-variant editions: 4 YAML configs (default/research/devops/brief)
- Variant engine: filter → rank → render (HTML + JSON, themed)
- Per-adapter timeout (10s) + threading fallback
- Consolidated 12 root scripts → thin wrappers + oracle/ package
- Archived stale scripts (_engagement, _live_compare, reddit_proof)
- Updated .gitignore, README.md, schema.sql
2026-07-22 13:32:15 +00:00

255 lines
8.0 KiB
Python

"""Multi-variant edition engine for Athena.
One data pipeline → multiple audience-specific editions.
Each variant is a YAML config that defines filters, ranking, and display.
World Monitor pattern: 6 variants from 1 codebase.
"""
import os
import sqlite3
import yaml
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from oracle.config import ROOT, DB_PATH, VERDICT_THRESHOLDS
# ── Variant config defaults ────────────────────────────────────────────────
DEFAULT_CONFIG = {
"name": "Athena AI News",
"description": "",
"output": "index.html",
"filters": {
"verdicts": ["PUBLISH", "WATCH", "ARCHIVE", "DROP"],
"sources": [],
"min_score": 0,
"max_age_h": 0,
"max_items": 0,
},
"ranking": {
"by": "clickability",
"half_life_h": 18,
},
"display": {
"top_n": 8,
"show_summary": True,
"show_score": True,
"show_tier": False,
"show_verdict": False,
"theme": "dark",
"accent": "#5b8cff",
"logo": "🏛️",
},
}
def load_variant(name: str) -> dict:
"""Load a variant config by name, merging with defaults.
Looks in variants/<name>.yaml relative to project root.
Returns merged config dict.
"""
variants_dir = ROOT / "variants"
variant_file = variants_dir / f"{name}.yaml"
if not variant_file.exists():
available = [p.stem for p in sorted(variants_dir.glob("*.yaml"))]
raise FileNotFoundError(
f"Variant '{name}' not found. Available: {', '.join(available)}"
)
with open(variant_file) as f:
raw = yaml.safe_load(f) or {}
# Deep merge with defaults
config = _deep_merge(DEFAULT_CONFIG, raw)
config["_name"] = name
config["_file"] = str(variant_file)
return config
def list_variants() -> list[str]:
"""List all available variant names."""
variants_dir = ROOT / "variants"
if not variants_dir.exists():
return []
return sorted(p.stem for p in variants_dir.glob("*.yaml"))
def apply_filters(conn: sqlite3.Connection, variant: dict) -> list[dict]:
"""Fetch entries from DB filtered by variant config.
Returns list of entry dicts matching the variant's filter criteria.
"""
f = variant["filters"]
# Build query
clauses = []
params = []
# Verdict filter
if f.get("verdicts"):
verdicts = [v for v in f["verdicts"] if v] # strip empties
if verdicts:
placeholders = ", ".join("?" for _ in verdicts)
clauses.append(f"verdict IN ({placeholders})")
params.extend(verdicts)
# Source filter (empty = all)
if f.get("sources"):
sources = [s for s in f["sources"] if s]
if sources:
placeholders = ", ".join("?" for _ in sources)
clauses.append(f"source IN ({placeholders})")
params.extend(sources)
# Min signal score
if f.get("min_score", 0) > 0:
clauses.append("COALESCE(signal_score, 0) >= ?")
params.append(f["min_score"])
# Max age
if f.get("max_age_h", 0) > 0:
max_age = f["max_age_h"]
cutoff = datetime.now(timezone.utc).timestamp() - (max_age * 3600)
clauses.append("first_seen >= datetime(?, 'unixepoch')")
params.append(cutoff)
# Base query
sql = "SELECT * FROM entries"
if clauses:
sql += " WHERE " + " AND ".join(clauses)
sql += " ORDER BY first_seen DESC"
# Max items
if f.get("max_items", 0) > 0:
sql += f" LIMIT {int(f['max_items'])}"
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute(sql, params)
rows = cur.fetchall()
return [dict(r) for r in rows]
def rank_items(items: list[dict], variant: dict) -> list[dict]:
"""Rank items according to variant ranking config.
Supports: clickability, signal_score, verdict_priority, freshness
"""
ranking = variant["ranking"]
by = ranking.get("by", "clickability")
half_life = ranking.get("half_life_h", 18)
if by == "clickability":
# Use clickability module for decayed scoring
from oracle.clickability import compute_index, decay_index
items = compute_index(items)
items = decay_index(items, half_life)
items.sort(key=lambda x: x.get("clickability_decayed", 0), reverse=True)
elif by == "signal_score":
items.sort(key=lambda x: float(x.get("signal_score") or 0), reverse=True)
elif by == "verdict_priority":
# PUBLISH > WATCH > ARCHIVE > DROP
priority = {"PUBLISH": 0, "WATCH": 1, "ARCHIVE": 2, "DROP": 3}
items.sort(key=lambda x: (
priority.get(x.get("verdict", "DROP"), 4),
float(x.get("signal_score") or 0),
), reverse=False)
elif by == "freshness":
items.sort(key=lambda x: x.get("first_seen", ""), reverse=True)
else:
# Default fallback: signal score
items.sort(key=lambda x: float(x.get("signal_score") or 0), reverse=True)
return items
def render_variant(variant: dict, dry_run: bool = False, webroot: Optional[str] = None):
"""Full render pipeline for one variant.
Load config → filter → rank → render HTML.
Returns (output_path, item_count).
"""
from oracle.render import render_variant_html, render_variant_json
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
items = apply_filters(conn, variant)
items = rank_items(items, variant)
conn.close()
if not items:
print(f"[variant:{variant['_name']}] No items matched filters")
return None, 0
page = render_variant_html(items, variant)
# Determine output path
output_rel = variant.get("output", "index.html")
if dry_run:
out_dir = ROOT / "_preview" / variant["_name"]
out_dir.mkdir(parents=True, exist_ok=True)
# Use just the filename for dry-run (output_rel may have subdirs)
dry_filename = os.path.basename(output_rel)
out_path = str(out_dir / dry_filename)
with open(out_path, "w") as f:
f.write(page)
# Also write JSON feed
json_path = str(out_dir / "feed.json")
render_variant_json(items, variant, json_path)
display = variant["display"]
top_n = display.get("top_n", 8)
top = items[:top_n]
print(f"[variant:{variant['_name']}] wrote {out_path} ({len(items)} items)")
print(f" TOP {top_n}:")
for i, it in enumerate(top, 1):
score = f" sig={it.get('signal_score', 0):.1f}" if display.get("show_score") else ""
tier = f" tier={it.get('source_tier', '?')}" if display.get("show_tier") else ""
verdict = f" [{it.get('verdict', '?')}]" if display.get("show_verdict") else ""
print(f" {i}. {it['title'][:60]}{score}{tier}{verdict}")
return out_path, len(items)
# Production render
target = webroot or str(ROOT / "site")
out_path = os.path.join(target, output_rel)
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
with open(out_path, "w") as f:
f.write(page)
# JSON feed alongside
json_path = os.path.join(os.path.dirname(out_path), "feed.json")
render_variant_json(items, variant, json_path)
print(f"[variant:{variant['_name']}] wrote {out_path} ({len(items)} items)")
return out_path, len(items)
# ── Helpers ────────────────────────────────────────────────────────────────
def _deep_merge(base: dict, override: dict) -> dict:
"""Recursively merge override into base dict."""
merged = base.copy()
for key, value in override.items():
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
merged[key] = _deep_merge(merged[key], value)
else:
merged[key] = value
return merged