Files
athena-oracle/adapters/rss_feeds.py
Epictetus 2c6701f5a3 Add RSS Feeds adapter (6th source) — commercial AI news from 10 feeds
Bug fix: date parsing was ISO-only (RFC 2822 feeds filtered out).
Added email.utils.parsedate_to_datetime() fallback for RSS dates.

10 feeds: TechCrunch AI, VentureBeat AI, The Verge AI, AI News,
The Decoder, MIT Tech Review AI, OpenAI Blog, Anthropic, Google AI, Meta AI.

3 dead feeds (Anthropic/Google/Meta 404), 7 working.
Score: authority (primary blogs 1.5x, industry 1.3x) × recency decay (48h half-life)
Age cutoff: 14 days. Score type: estimated.

Wired into pipeline.py ENABLED_SOURCES. Added RSS URL verification.
3/3 spot-checks passed (all URLs reachable, HTTP 200).

Commit: 62031be→c24a89f
2026-07-08 15:27:59 +00:00

283 lines
11 KiB
Python

#!/usr/bin/env python3
"""
RSS Feeds adapter for Athena.
Ingests commercial AI news from curated RSS feeds — no auth, no scraping,
reliable structured data.
Fills the "commercial AI news" gap: corporate moves, funding, product launches,
partnerships, policy. Genuinely different signal from GitHub/arXiv/Reddit/HN/HF.
Feeds selected for AI relevance, reliability, and freshness. Techmeme skipped
(requires JS rendering). Axios/The Decoder kept despite 3xx status codes
(feedparser follows redirects fine).
Usage:
python3 adapters/rss_feeds.py
"""
import json
import os
import re
import time
import sqlite3
import feedparser
from datetime import datetime, timedelta, timezone
from email.utils import parsedate_to_datetime
from adapters import SourceAdapter
# Curated feed list — AI-focused, reliable, diverse publishers.
# Format: (source_key, label, url)
# Each source_key becomes the `source` field in the DB.
FEEDS = [
# Industry news
("rss:techcrunch", "TechCrunch AI",
"https://techcrunch.com/category/artificial-intelligence/feed/"),
("rss:venturebeat", "VentureBeat AI",
"https://venturebeat.com/category/ai/feed/"),
("rss:theverge", "The Verge AI",
"https://www.theverge.com/rss/ai-artificial-intelligence/index.xml"),
("rss:ainews", "AI News",
"https://www.artificialintelligence-news.com/feed/"),
# Analysis
("rss:decoder", "The Decoder",
"https://www.the-decoder.com/feed/"),
("rss:mittr", "MIT Tech Review AI",
"https://www.technologyreview.com/topic/artificial-intelligence/feed/"),
# Company blogs (primary signals for launches)
("rss:openai", "OpenAI Blog",
"https://openai.com/blog/rss.xml"),
("rss:anthropic", "Anthropic News",
"https://www.anthropic.com/rss/news.xml"),
("rss:googleai", "Google AI Blog",
"https://blog.google/technology/rss.xml"),
("rss:metaai", "Meta AI Blog",
"https://ai.meta.com/blog/rss.xml"),
]
# AI relevance keywords for filtering — word-boundary matching
# to avoid false positives ("Great Britain" != "AI")
AI_KEYWORDS = [
# Core AI terms
r"\bai\b", r"\bmachine learning\b", r"\bdeep learning\b", r"\bneural\b",
r"\bgenerative ai\b", r"\bgenerative\b", r"\bllm\b", r"\blarge language\b",
r"\bfoundation model\b", r"\btransformer\b", r"\baugmented\b",
# Agents & automation
r"\bagent\b", r"\bautonomous\b", r"\bmcp\b", r"\bfunction call\b",
r"\btool use\b", r"\brai\b", r"\bretrieval\b",
# Models & inference
r"\binference\b", r"\bmodel\b", r"\bembedding\b", r"\btoken\b",
r"\bfine-tune\b", r"\btraining\b", r"\bquantiz\b",
# Specific models (current landscape)
r"\bclaude\b", r"\bgpt\b", r"\bgemini\b", r"\bllama\b", r"\bdeepseek\b",
r"\bqwen\b", r"\bmistral\b", r"\bcodex\b", r"\bchatgpt\b",
r"\bsora\b", r"\bkimi\b", r"\bglm\b", r"\bminimax\b",
# Companies
r"\bopenai\b", r"\banthropic\b", r"\bgoogle\b", r"\bmeta\b",
r"\bmicrosoft\b", r"\bnvidia\b", r"\bamazon\b", r"\baws\b",
r"\bsamba(?=\w*nova\b)", r"\bcohere\b", r"\baider\b",
]
class RSSFeedsAdapter(SourceAdapter):
"""RSS feed aggregator for commercial AI news."""
def name(self) -> str:
return "rss"
def _is_ai_relevant(self, title: str, summary: str, tags: list) -> bool:
"""Check if an entry is AI-relevant using word-boundary keyword matching."""
text = (title + " " + summary).lower()
tag_text = " ".join(tags).lower()
combined = text + " " + tag_text
for pattern in AI_KEYWORDS:
if re.search(pattern, combined):
return True
return False
def _score(self, entry: dict, source_label: str, age_hours: float) -> float:
"""Score: source authority + recency. Estimated (no native popularity metric).
Source authority weights (based on AI signal quality):
- OpenAI/Anthropic/Google/Meta blogs: 1.5 (primary signals)
- TechCrunch/VentureBeat: 1.3 (industry news)
- The Verge/AI News/The Decoder/MIT TR: 1.0 (general coverage)
Recency decay: exponential half-life of 48 hours.
"""
# Source authority
authority = 1.0
primary_blogs = ("openai", "anthropic", "googleai", "metaai")
industry_news = ("techcrunch", "venturebeat")
source_key = entry.get("source_key", "")
if source_key in primary_blogs:
authority = 1.5
elif source_key in industry_news:
authority = 1.3
# Recency decay: half-life = 48 hours
import math
recency = 2 ** (-age_hours / 48.0) # 1.0 at t=0, 0.5 at 48h, 0.25 at 96h
# Tag bonus: more tags = more signal
tag_count = len(entry.get("tags", []))
tag_bonus = min(tag_count * 0.1, 0.5) # max +0.5
score = (authority * recency * 4.0) + tag_bonus
return min(round(score, 2), 10.0)
def _tags(self, entry: dict) -> list:
"""Generate category tags from feed entry metadata."""
tags = ["rss", entry.get("source_key", "rss:unknown")]
# Add feed-provided tags
for t in entry.get("tags", []):
clean = re.sub(r"[^a-z0-9-]", "-", t.lower())
tags.append(f"tag:{clean}")
return tags
def fetch(self, query: str = "", limit: int = 20) -> list[dict]:
"""Fetch entries from all configured RSS feeds.
Returns the top `limit` entries across all feeds, sorted by score.
Only AI-relevant entries are included.
"""
now = datetime.now(timezone.utc)
all_entries = []
feed_failures = []
for source_key, label, url in FEEDS:
try:
d = feedparser.parse(url)
if d.status not in (200, 301, 302, 307, 308) or not d.entries:
feed_failures.append(f"{source_key}: status={d.status}, entries=0")
continue
for entry in d.entries:
if not self._is_ai_relevant(
entry.get("title", ""),
entry.get("summary", ""),
[t.get("term", "") for t in entry.get("tags", [])],
):
continue
# Parse publish time — handle RFC 2822 (most RSS) and ISO 8601
published = entry.get("published", entry.get("updated", ""))
age_hours = 999 # default: old
if published:
try:
pub_dt = parsedate_to_datetime(published)
except (ValueError, TypeError):
try:
pub_dt = datetime.fromisoformat(published.replace("Z", "+00:00"))
except (ValueError, TypeError):
pass
else:
age_hours = max((now - pub_dt).total_seconds() / 3600, 0.1)
# Skip entries older than 14 days
if age_hours > 14 * 24:
continue
tags = [t.get("term", "") for t in entry.get("tags", [])]
entry_dict = {
"source_key": source_key,
"source_label": label,
"age_hours": age_hours,
"tags": tags,
}
score = self._score(entry_dict, label, age_hours)
entry_dict["score"] = score
all_entries.append({
"source": "rss",
"source_id": f"{source_key}:{entry.get('id', entry.get('link', ''))[-30:]}",
"url": entry.get("link", ""),
"title": entry.get("title", ""),
"extracted_text": entry.get("summary", ""),
"summary": None, # LLM later
"category_tags": json.dumps(self._tags(entry_dict)),
"signal_score": score,
"raw_metadata": json.dumps({
"source_key": source_key,
"source_label": label,
"author": entry.get("author", ""),
"published": published,
"age_hours": round(age_hours, 1),
"tags": tags,
"score_type": "estimated",
}),
"first_seen": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
"last_updated": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
})
time.sleep(0.5) # polite spacing
except Exception as e:
feed_failures.append(f"{source_key}: {e}")
if feed_failures:
print(f" ⚠ Feed failures: {', '.join(feed_failures)}")
# Sort by score descending, take top N
all_entries.sort(key=lambda e: e["signal_score"], reverse=True)
return all_entries[:limit]
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="RSS feeds adapter for Athena")
parser.add_argument("--limit", type=int, default=20, help="Max entries")
parser.add_argument("--dry-run", action="store_true", help="Don't store in DB")
parser.add_argument("--db", default=os.path.join(os.path.dirname(__file__), "..", "oracle.db"))
parser.add_argument("--schema", default=os.path.join(os.path.dirname(__file__), "..", "schema.sql"))
args = parser.parse_args()
print(f"=== RSS Feeds Adapter ({len(FEEDS)} feeds) ===")
adapter = RSSFeedsAdapter()
entries = adapter.fetch(limit=args.limit * 2) # fetch extra for relevance filtering
entries = entries[:args.limit]
print(f"\n Fetched {len(entries)} AI-relevant entries from {len(FEEDS)} feeds")
print()
if entries:
print(f" {'Rank':<4} {'Score':<7} {'Age':<8} {'Source':<16} {'Title'}")
print(" " + "-" * 90)
for i, e in enumerate(entries[:15], 1):
meta = json.loads(e["raw_metadata"])
print(f" {i:<4} {e['signal_score']:<7.2f} {meta['age_hours']:<8.1f}h {meta['source_label']:<16} {e['title'][:70]}")
if not args.dry_run:
conn = sqlite3.connect(args.db)
if os.path.exists(args.schema):
with open(args.schema) as f:
conn.executescript(f.read())
conn.commit()
cur = conn.cursor()
stored = 0
for entry in entries:
try:
cur.execute("""
INSERT OR REPLACE INTO entries
(source, source_id, url, title, extracted_text, summary,
category_tags, signal_score, raw_metadata, first_seen, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
entry["source"], entry["source_id"], entry["url"], entry["title"],
entry["extracted_text"], entry["summary"],
entry["category_tags"], entry["signal_score"],
entry["raw_metadata"], entry["first_seen"], entry["last_updated"],
))
stored += 1
except Exception as e:
print(f" DB error: {e}")
conn.commit()
conn.close()
print(f"\n Stored {stored} entries")
print(f"\n Done.")