Files
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

204 lines
6.4 KiB
Python

"""Database operations for the AI Research Oracle.
Handles connections, schema initialization, and common queries.
"""
import os
import sqlite3
from typing import Optional
from oracle.config import DB_PATH, SCHEMA_PATH, SOURCE_TIERS
def get_connection(db_path: Optional[str] = None) -> sqlite3.Connection:
"""Open a database connection."""
path = db_path or str(DB_PATH)
conn = sqlite3.connect(path)
conn.row_factory = sqlite3.Row
return conn
def get_ro_connection(db_path: Optional[str] = None) -> sqlite3.Connection:
"""Open a read-only database connection."""
path = db_path or str(DB_PATH)
return sqlite3.connect(f"file:{path}?mode=ro", uri=True)
def init_db(db_path: Optional[str] = None, schema_path: Optional[str] = None) -> sqlite3.Connection:
"""Initialize or open the database, applying schema if it exists.
Schema uses CREATE IF NOT EXISTS so repeated calls are idempotent.
Also runs World Monitor migration columns (content_hash, verdict, freshness).
"""
conn = sqlite3.connect(db_path or str(DB_PATH))
sp = schema_path or str(SCHEMA_PATH)
if os.path.exists(sp):
with open(sp) as f:
conn.executescript(f.read())
conn.commit()
# World Monitor migration columns (idempotent)
for col in [
"content_hash TEXT DEFAULT ''",
"verdict TEXT DEFAULT ''",
"source_tier INTEGER DEFAULT 2",
]:
try:
conn.execute(f"ALTER TABLE entries ADD COLUMN {col}")
except sqlite3.OperationalError:
pass # already exists
conn.commit()
return conn
def migrate_world_monitor(conn: Optional[sqlite3.Connection] = None) -> dict:
"""Apply World Monitor schema migrations + backfill.
Returns: {columns_added: int, hashes_backfilled: int, verdicts_set: int}
"""
from oracle.dedup import backfill_hashes, apply_verdicts, get_source_tier
c = conn or get_connection()
cur = c.cursor()
# Check which columns already exist
cur.execute("PRAGMA table_info(entries)")
existing = {row[1] for row in cur.fetchall()}
columns_to_add = []
if "content_hash" not in existing:
columns_to_add.append("content_hash TEXT DEFAULT ''")
if "verdict" not in existing:
columns_to_add.append("verdict TEXT DEFAULT ''")
if "source_tier" not in existing:
columns_to_add.append("source_tier INTEGER DEFAULT 2")
added = 0
for col_def in columns_to_add:
try:
cur.execute(f"ALTER TABLE entries ADD COLUMN {col_def}")
added += 1
except sqlite3.OperationalError:
pass # race condition or already exists
c.commit()
# Backfill content hashes
hashes = backfill_hashes(c)
# Backfill source tiers — reset first so the WHERE clause catches everything
cur.execute("UPDATE entries SET source_tier = 0")
c.commit()
for source_name, tier_info in SOURCE_TIERS.items():
cur.execute(
"UPDATE entries SET source_tier = ? WHERE source = ?",
(tier_info["tier"], source_name),
)
c.commit()
# Set verdicts
verdicts = apply_verdicts(c)
return {"columns_added": added, "hashes_backfilled": hashes, "verdicts_set": verdicts}
def get_stats(conn: sqlite3.Connection) -> dict:
"""Return database statistics."""
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM entries")
total = cur.fetchone()[0]
cur.execute("""
SELECT source, COUNT(*) as cnt, ROUND(AVG(signal_score), 2) as avg_score,
MIN(signal_score) as min_score, MAX(signal_score) as max_score
FROM entries GROUP BY source
""")
sources = {r["source"]: dict(r) for r in cur.fetchall()}
cur.execute("SELECT COUNT(*) FROM entries WHERE summary IS NOT NULL")
summarized = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM entries WHERE summary IS NULL")
pending = cur.fetchone()[0]
# Bucket distribution
try:
cur.execute("""
SELECT bucket, COUNT(*) as cnt
FROM entries WHERE bucket IS NOT NULL
GROUP BY bucket ORDER BY cnt DESC
""")
buckets = {r["bucket"]: r["cnt"] for r in cur.fetchall()}
except Exception:
buckets = {}
return {
"total_entries": total,
"sources": sources,
"summarized": summarized,
"pending_summary": pending,
"buckets": buckets,
}
def query_top(conn: sqlite3.Connection, n: int = 10,
source: Optional[str] = None,
min_score: float = 0) -> list[dict]:
"""Get top N entries by signal score."""
cur = conn.cursor()
where_parts = []
params = []
if min_score > 0:
where_parts.append("signal_score >= ?")
params.append(min_score)
if source:
where_parts.append("source = ?")
params.append(source)
where = (" AND " + " AND ".join(where_parts)) if where_parts else ""
cur.execute(
f"SELECT * FROM entries {where} ORDER BY signal_score DESC LIMIT ?",
params + [n],
)
return [dict(r) for r in cur.fetchall()]
def query_recent(conn: sqlite3.Connection, hours: int = 24) -> list[dict]:
"""Get entries from the last N hours."""
from datetime import datetime, timezone, timedelta
cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
cur = conn.cursor()
cur.execute(
"SELECT * FROM entries WHERE first_seen >= ? ORDER BY first_seen DESC",
(cutoff,),
)
return [dict(r) for r in cur.fetchall()]
def query_search(conn: sqlite3.Connection, q: str, limit: int = 20) -> list[dict]:
"""Search entries by title, summary, and key technical point."""
cur = conn.cursor()
pattern = f"%{q}%"
cur.execute("""
SELECT * FROM entries
WHERE title LIKE ?
OR json_extract(summary,'$.one_liner') LIKE ?
OR json_extract(summary,'$.key_technical_point') LIKE ?
ORDER BY signal_score DESC LIMIT ?
""", (pattern, pattern, pattern, limit))
return [dict(r) for r in cur.fetchall()]
def query_by_tag(conn: sqlite3.Connection, tag: str, limit: int = 20) -> list[dict]:
"""Get entries matching a category tag."""
cur = conn.cursor()
cur.execute("""
SELECT * FROM entries
WHERE json_extract(category_tags,'$') LIKE ?
ORDER BY signal_score DESC LIMIT ?
""", (f'%"{tag}"%', limit))
return [dict(r) for r in cur.fetchall()]