"""Daily digest: 10 current articles on AI agents from reputable outlets, each with a short original summary (written by the local model from the article's own text) and the source link. Never stores or shows the article text itself.""" from __future__ import annotations import datetime as dt import email.utils import re import xml.etree.ElementTree as ET from . import llm, safefetch, text from .store import LOCK, Store TZ = dt.timezone(dt.timedelta(hours=8)) FEEDS = [ ("TechCrunch", "https://techcrunch.com/category/artificial-intelligence/feed/"), ("The Verge", "https://www.theverge.com/rss/ai-artificial-intelligence/index.xml"), ("VentureBeat", "https://venturebeat.com/category/ai/feed/"), ("Ars Technica", "https://feeds.arstechnica.com/arstechnica/technology-lab"), ("MIT Technology Review", "https://www.technologyreview.com/topic/artificial-intelligence/feed"), ("The Register", "https://www.theregister.com/software/ai_ml/headlines.atom"), ("Wired", "https://www.wired.com/feed/tag/ai/latest/rss"), ("Hugging Face", "https://huggingface.co/blog/feed.xml"), ("Google", "https://blog.google/technology/ai/rss/"), ("OpenAI", "https://openai.com/news/rss.xml"), ("Microsoft", "https://blogs.microsoft.com/ai/feed/"), ("AWS Machine Learning", "https://aws.amazon.com/blogs/machine-learning/feed/"), ("ZDNET", "https://www.zdnet.com/topic/artificial-intelligence/rss.xml"), ("SiliconANGLE", "https://siliconangle.com/category/ai/feed/"), ] STRONG = re.compile(r"\b(ai agents?|agentic|multi-agent|autonomous agents?|agent-to-agent|a2a|model context protocol|mcp|" r"computer[- ]use|coding agents?|agent (?:framework|platform|economy|ecosystem|marketplace|protocol)|agents?)\b", re.I) AI = re.compile(r"\b(ai|llm|model|openai|anthropic|claude|gemini|gpt|copilot|automation)\b", re.I) MAX_PER_SOURCE = 3 MAX_AGE_DAYS = 3 SUMMARY_SYSTEM = ( "You write short, original news summaries for a digest about AI agents. Use only facts in the provided " "article text. Write 2 or 3 plain sentences (max 70 words) in your own words. Do not quote sentences " "from the article, do not use markdown, do not add opinions or facts that are not in the text. " "End with one short sentence starting 'Why it matters:' about the relevance to AI agents.") def _parse_date(s: str | None): if not s: return None s = s.strip() try: d = email.utils.parsedate_to_datetime(s) except (TypeError, ValueError): try: d = dt.datetime.fromisoformat(s.replace("Z", "+00:00")) except ValueError: return None if d.tzinfo is None: d = d.replace(tzinfo=dt.timezone.utc) return d def parse_feed(body: bytes, source: str) -> list[dict]: if b" int: t, d = item["title"], item.get("teaser", "") score = 3 * len(STRONG.findall(t)) + len(STRONG.findall(d)) if score and AI.search(t + " " + d): score += 1 return score def candidates(now: dt.datetime, seen: set[str], fetch=safefetch.fetch, log=print) -> list[dict]: items = [] for source, url in FEEDS: try: _, _, body = fetch(url) except safefetch.FetchError as exc: log(f"feed {source}: {exc}") continue got = parse_feed(body, source) log(f"feed {source}: {len(got)} items") items += got fresh = [] for it in items: if it["url"] in seen or not it["published"]: continue age = now - it["published"] if age.days >= MAX_AGE_DAYS or age.total_seconds() < -3600: continue it["score"] = relevance(it) if it["score"] >= 2: fresh.append(it) fresh.sort(key=lambda i: (i["score"], i["published"]), reverse=True) picked, per, titles = [], {}, set() for it in fresh: key = re.sub(r"\W+", " ", it["title"].lower())[:60] if per.get(it["source"], 0) >= MAX_PER_SOURCE or key in titles: continue per[it["source"]] = per.get(it["source"], 0) + 1 titles.add(key) picked.append(it) return picked def summarise(it: dict, fetch=safefetch.fetch, chat=llm.chat) -> str | None: article = it.get("teaser", "") try: _, ctype, body = fetch(it["url"]) info = text.page_info(body, ctype) if len(info["text"]) > len(article): article = info["text"] except safefetch.FetchError: pass if len(article) < 120: return None out = chat(SUMMARY_SYSTEM, f"Title: {it['title']}\nSource: {it['source']}\n\nArticle text:\n{article[:3500]}", max_tokens=220) summary = text.plain(out.replace("*", ""), 600) if not summary or copied(summary, article): return None return summary def copied(summary: str, article: str, n: int = 12) -> bool: """True if the summary reuses any 12-word run from the article (we never reproduce text).""" w = re.findall(r"\w+", article.lower()) grams = {" ".join(w[i:i + n]) for i in range(max(0, len(w) - n + 1))} s = re.findall(r"\w+", summary.lower()) return any(" ".join(s[i:i + n]) in grams for i in range(max(0, len(s) - n + 1))) def build(store: Store, *, now: dt.datetime | None = None, count: int = 10, fetch=safefetch.fetch, chat=llm.chat, log=print) -> dict: now = now or dt.datetime.now(TZ) day = now.astimezone(TZ).date().isoformat() # never repeat an article from an earlier day (a re-run of today may reuse today's picks) seen = {e["url"] for k, d in store.data["digests"].items() if k != day for e in d.get("entries", []) + d.get("community", [])} entries = [] for it in candidates(now, seen, fetch, log): if len(entries) >= count: break try: s = summarise(it, fetch, chat) except RuntimeError as exc: log(f"summary failed: {exc}") s = None if not s: continue entries.append({"title": it["title"], "url": it["url"], "source": it["source"], "summary": s, "published": it["published"].isoformat(), "kind": "digest"}) log(f"picked {len(entries)}: {it['source']} | {it['title']}") with store.tx() as data: d = data["digests"].setdefault(day, {"date": day, "entries": [], "community": []}) d["entries"] = entries d["generated_at"] = now.isoformat(timespec="seconds") return d