This repository has been archived on 2026-07-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Hitonabi b75335dafd v5 Phase 3: News-Quellen aufgeraeumt + Upgrade-Vorschlaege
- news.py: Git-Release-Feeds raus (b9745-Rauschen), Qualitaetsquellen rein
  (HuggingFace, r/LocalLLaMA, Simon Willison, Latent Space, Ahead of AI, The Batch).
- cookbook.py: /upgrades (matcht installierte alte Modelle gegen UPGRADES-Map) +
  /install-model (einzelnes Modell tauschen, GGUF dynamisch, optimaler ctx).
- news.js: 'Fuer dich: Upgrades'-Sektion oben (Modell X ersetzt dein Y, weil ... [Installieren]).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:18:37 +02:00

100 lines
3.0 KiB
Python

"""
News-Router: aggregiert kuratierte RSS/Atom-Feeds rund um lokale LLMs.
Serverseitig geholt (httpx) + mit stdlib geparst (kein neuer Dep), In-memory-Cache (30 min).
Degradiert sauber: einzelne Feeds dürfen fehlschlagen, der Rest kommt trotzdem.
"""
import time
import xml.etree.ElementTree as ET
from datetime import datetime
from email.utils import parsedate_to_datetime
import httpx
from fastapi import APIRouter, Depends
from auth import auth
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
FEEDS = [
("HuggingFace", "https://huggingface.co/blog/feed.xml"),
("r/LocalLLaMA", "https://www.reddit.com/r/LocalLLaMA/.rss"),
("Simon Willison", "https://simonwillison.net/atom/everything/"),
("Latent Space", "https://www.latent.space/feed"),
("Ahead of AI", "https://magazine.sebastianraschka.com/feed"),
("The Batch", "https://www.deeplearning.ai/the-batch/rss.xml"),
]
_UA = {"User-Agent": "Mozilla/5.0 (MissionControl NewsBot)"}
_TTL = 1800 # 30 min
_CACHE = {"ts": 0.0, "items": []}
_RELEVANT = ("rocm", "gfx1151", "strix", "amd", "vulkan", "llama.cpp", "gguf", "quant")
def _tag(t):
return t.split("}")[-1]
def _epoch(s):
if not s:
return 0.0
try:
return parsedate_to_datetime(s).timestamp()
except Exception: # noqa: BLE001
pass
try:
return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp()
except Exception: # noqa: BLE001
return 0.0
def _parse(source, xml_text):
out = []
try:
root = ET.fromstring(xml_text)
except Exception: # noqa: BLE001
return out
for el in root.iter():
if _tag(el.tag) not in ("item", "entry"):
continue
title = link = date = ""
for ch in el:
t = _tag(ch.tag)
if t == "title":
title = (ch.text or "").strip()
elif t == "link":
link = ch.get("href") or (ch.text or "").strip() or link
elif t in ("pubDate", "published", "updated") and not date:
date = (ch.text or "").strip()
if title:
low = title.lower()
out.append({
"source": source, "title": title, "link": link,
"ts": _epoch(date),
"relevant": any(k in low for k in _RELEVANT),
})
return out[:8]
def get_news():
now = time.time()
if now - _CACHE["ts"] < _TTL and _CACHE["items"]:
return _CACHE["items"]
items = []
with httpx.Client(timeout=8.0, headers=_UA, follow_redirects=True) as c:
for source, url in FEEDS:
try:
r = c.get(url)
r.raise_for_status()
items += _parse(source, r.text)
except Exception: # noqa: BLE001
continue
items.sort(key=lambda x: x["ts"], reverse=True)
items = items[:24]
_CACHE.update(ts=now, items=items)
return items
@router.get("/news")
def news():
return {"items": get_news()}