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
This commit is contained in:
+28
-2
@@ -89,8 +89,9 @@ def true_first_seen(raw_meta, source, now_str):
|
||||
UPSERT_SQL = """
|
||||
INSERT INTO entries
|
||||
(source, source_id, url, title, extracted_text, summary,
|
||||
category_tags, signal_score, raw_metadata, first_seen, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
category_tags, signal_score, raw_metadata, first_seen, last_updated,
|
||||
content_hash, source_tier, verdict)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(source, source_id) DO UPDATE SET
|
||||
source = excluded.source,
|
||||
source_id = excluded.source_id,
|
||||
@@ -101,6 +102,9 @@ ON CONFLICT(source, source_id) DO UPDATE SET
|
||||
signal_score = excluded.signal_score,
|
||||
raw_metadata = excluded.raw_metadata,
|
||||
last_updated = excluded.last_updated,
|
||||
content_hash = excluded.content_hash,
|
||||
source_tier = excluded.source_tier,
|
||||
verdict = excluded.verdict,
|
||||
first_seen = COALESCE((SELECT first_seen FROM entries WHERE source = excluded.source AND source_id = excluded.source_id), excluded.first_seen)
|
||||
"""
|
||||
|
||||
@@ -111,16 +115,38 @@ def upsert_entries(conn, entries):
|
||||
`entries` is the list of dicts as built by each adapter; each dict must
|
||||
already have first_seen set to the TRUE publish date (via true_first_seen)
|
||||
and last_updated to the harvest time.
|
||||
|
||||
Now also sets content_hash and source_tier at insertion time.
|
||||
Returns count of rows written.
|
||||
"""
|
||||
from oracle.dedup import content_hash, get_source_tier, compute_verdict, age_hours
|
||||
|
||||
cur = conn.cursor()
|
||||
written = 0
|
||||
for e in entries:
|
||||
# Compute content hash
|
||||
title = e.get("title", "")
|
||||
url = e.get("url", "")
|
||||
body = e.get("extracted_text", "")[:500]
|
||||
h = content_hash(title, url, body)
|
||||
|
||||
# Get source tier
|
||||
source = e.get("source", "")
|
||||
tier_info = get_source_tier(source)
|
||||
tier = tier_info["tier"]
|
||||
|
||||
# Compute verdict
|
||||
first_seen = e.get("first_seen", "")
|
||||
score = float(e.get("signal_score") or 0)
|
||||
age = age_hours(first_seen)
|
||||
verdict = compute_verdict(score, age)
|
||||
|
||||
cur.execute(UPSERT_SQL, (
|
||||
e["source"], e["source_id"], e["url"], e["title"],
|
||||
e.get("extracted_text"), e.get("summary"),
|
||||
e.get("category_tags"), e.get("signal_score"),
|
||||
e.get("raw_metadata"), e["first_seen"], e["last_updated"],
|
||||
h, tier, verdict,
|
||||
))
|
||||
written += 1
|
||||
conn.commit()
|
||||
|
||||
@@ -355,6 +355,18 @@ class ArxivAdapter(SourceAdapter):
|
||||
if paper.get("_applied_domain"):
|
||||
tags.append(paper["_applied_domain"])
|
||||
|
||||
# Local-serving / efficient-inference signal (suggested source: arXiv cs.LG
|
||||
# MoE/quantization papers → "local-serving" tag filter)
|
||||
serving_kw = [
|
||||
"quantiz", "quantization", "moe", "mixture of experts",
|
||||
"serving", "efficient inference", "pruning", "distill",
|
||||
"knowledge distillation", "low-rank", "lora", "parameter-efficient",
|
||||
"vram", "memory efficient", "edge inference", "on-device",
|
||||
]
|
||||
combined = f"{title_lower} {summary_lower}"
|
||||
if any(kw in combined for kw in serving_kw):
|
||||
tags.append("local-serving")
|
||||
|
||||
return tags
|
||||
|
||||
def fetch(self, query: str = "", limit: int = 20) -> list[dict]:
|
||||
|
||||
+43
-23
@@ -15,7 +15,9 @@ Strategy: 3s spacing between subreddits, retry with backoff.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import urllib.request
|
||||
@@ -51,11 +53,11 @@ class RedditAdapter(SourceAdapter):
|
||||
"automoderator",
|
||||
}
|
||||
|
||||
def __init__(self, subreddits=None, rate_limit=1, user_agent=None):
|
||||
def __init__(self, subreddits=None, rate_limit=4, user_agent=None):
|
||||
"""
|
||||
Args:
|
||||
subreddits: List of subreddit names.
|
||||
rate_limit: Seconds between subreddit requests.
|
||||
rate_limit: Base seconds between subreddit requests (with jitter).
|
||||
user_agent: Custom User-Agent header.
|
||||
"""
|
||||
self.subreddits = subreddits or self.DEFAULT_SUBREDDITS
|
||||
@@ -65,6 +67,12 @@ class RedditAdapter(SourceAdapter):
|
||||
def name(self) -> str:
|
||||
return "reddit"
|
||||
|
||||
def _sleep_with_jitter(self, base=None):
|
||||
"""Sleep with ±30% jitter to avoid pattern detection."""
|
||||
base = base or self.rate_limit
|
||||
jitter = base * 0.3 * (2 * random.random() - 1) # ±30%
|
||||
time.sleep(base + jitter)
|
||||
|
||||
def _clean_html(self, html: str) -> str:
|
||||
"""Extract readable text from Reddit's HTML content."""
|
||||
if not html:
|
||||
@@ -106,7 +114,8 @@ class RedditAdapter(SourceAdapter):
|
||||
url = f"https://www.reddit.com/r/{subreddit}/hot/.rss?limit=50"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": self.user_agent})
|
||||
|
||||
for attempt in range(2): # max 2 attempts, fail fast
|
||||
backoff = [3, 8] # staggered backoff: 3s then 8s
|
||||
for attempt in range(3): # max 3 attempts
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
xml_data = resp.read().decode("utf-8")
|
||||
@@ -116,8 +125,10 @@ class RedditAdapter(SourceAdapter):
|
||||
print(f" RSS blocked (HTTP {e.code}) for r/{subreddit}")
|
||||
return []
|
||||
if e.code == 429:
|
||||
if attempt == 0:
|
||||
time.sleep(2) # single retry with short backoff
|
||||
if attempt < len(backoff):
|
||||
delay = backoff[attempt]
|
||||
print(f" RSS 429 for r/{subreddit}, retrying in {delay}s")
|
||||
time.sleep(delay)
|
||||
continue
|
||||
print(f" RSS rate-limited for r/{subreddit}, skip")
|
||||
return []
|
||||
@@ -127,7 +138,7 @@ class RedditAdapter(SourceAdapter):
|
||||
print(f" RSS error r/{subreddit}: {e}")
|
||||
return []
|
||||
else:
|
||||
print(f" r/{subreddit}: still rate limited, skip")
|
||||
print(f" r/{subreddit}: still rate limited after 3 attempts, skip")
|
||||
return []
|
||||
|
||||
# Parse Atom XML
|
||||
@@ -301,6 +312,17 @@ class RedditAdapter(SourceAdapter):
|
||||
]):
|
||||
tags.append("meta:virality")
|
||||
|
||||
# Local-inference / on-device signal (suggested source: r/MachineLearning
|
||||
# "I tried X on-device" posts — high builder signal → local-inference feed)
|
||||
on_device_kw = [
|
||||
"on-device", "on device", "local inference", "local llm",
|
||||
"ran locally", "running locally", "in my pocket", "on my phone",
|
||||
"edge device", "offline", "no gpu", "consumer gpu", "rtx",
|
||||
"single gpu", "self-host", "self host",
|
||||
]
|
||||
if any(kw in title_lower or kw in content for kw in on_device_kw):
|
||||
tags.append("local-inference")
|
||||
|
||||
return tags
|
||||
|
||||
def _fetch_rss(self, subreddit: str) -> list[dict]:
|
||||
@@ -308,7 +330,8 @@ class RedditAdapter(SourceAdapter):
|
||||
url = f"https://www.reddit.com/r/{subreddit}/hot/.rss?limit=50"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": self.user_agent})
|
||||
|
||||
for attempt in range(2): # max 2 attempts, fail fast
|
||||
backoff = [3, 8] # staggered backoff: 3s then 8s
|
||||
for attempt in range(3): # max 3 attempts
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
xml_data = resp.read().decode("utf-8")
|
||||
@@ -318,8 +341,10 @@ class RedditAdapter(SourceAdapter):
|
||||
print(f" RSS blocked (HTTP {e.code}) for r/{subreddit}")
|
||||
return []
|
||||
if e.code == 429:
|
||||
if attempt == 0:
|
||||
time.sleep(2) # single retry with short backoff
|
||||
if attempt < len(backoff):
|
||||
delay = backoff[attempt]
|
||||
print(f" RSS 429 for r/{subreddit}, retrying in {delay}s")
|
||||
time.sleep(delay)
|
||||
continue
|
||||
print(f" RSS rate-limited for r/{subreddit}, skip")
|
||||
return []
|
||||
@@ -329,7 +354,7 @@ class RedditAdapter(SourceAdapter):
|
||||
print(f" RSS error r/{subreddit}: {e}")
|
||||
return []
|
||||
else:
|
||||
print(f" r/{subreddit}: still rate limited, skip")
|
||||
print(f" r/{subreddit}: still rate limited after 3 attempts, skip")
|
||||
return []
|
||||
|
||||
# Parse Atom XML
|
||||
@@ -373,31 +398,26 @@ class RedditAdapter(SourceAdapter):
|
||||
Filters AutoModerator and sticky posts.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
# Try JSON first — if it's blocked on the first subreddit, bail fast
|
||||
# rather than wasting time on all subreddits
|
||||
# Try JSON first — if it's blocked on the first subreddit, skip
|
||||
# the test-RSS call (which would waste a request and risk rate-limiting)
|
||||
# and go straight to the RSS loop
|
||||
first_json = self._try_json(self.subreddits[0])
|
||||
if not first_json:
|
||||
# JSON is blocked site-wide, try one RSS to confirm
|
||||
test_rss = self._fetch_rss(self.subreddits[0])
|
||||
if not test_rss:
|
||||
print(" Reddit blocked (403/429), returning empty")
|
||||
return []
|
||||
# RSS works — fall through to full fetch below
|
||||
json_worked = bool(first_json)
|
||||
|
||||
all_entries = []
|
||||
seen_ids = set()
|
||||
json_worked = bool(first_json)
|
||||
|
||||
# Add first JSON results
|
||||
# Add first JSON results if any
|
||||
for p in first_json:
|
||||
if p["id"] not in seen_ids:
|
||||
seen_ids.add(p["id"])
|
||||
all_entries.append(p)
|
||||
time.sleep(self.rate_limit)
|
||||
|
||||
# If JSON didn't work, fall back to RSS for all subreddits
|
||||
if not json_worked:
|
||||
print(" JSON endpoints blocked, using RSS fallback")
|
||||
# Brief cooldown before RSS barrage
|
||||
time.sleep(3)
|
||||
for sub in self.subreddits:
|
||||
entries = self._fetch_rss(sub)
|
||||
for e in entries:
|
||||
@@ -422,7 +442,7 @@ class RedditAdapter(SourceAdapter):
|
||||
if entry["id"] not in seen_ids:
|
||||
seen_ids.add(entry["id"])
|
||||
all_entries.append(entry)
|
||||
time.sleep(self.rate_limit)
|
||||
self._sleep_with_jitter()
|
||||
|
||||
# Filter sticky/mod posts
|
||||
filtered = []
|
||||
|
||||
Reference in New Issue
Block a user