DS DevShelfHub Projects · AI tools

Personalized News Digest: AI-Curated Daily Briefs From Live RSS Feeds

Intermediate

By DevShelfHub

Select topics, pick a reading tone (beginner-friendly / technical / executive brief), and get a curated daily digest pulled from live RSS feeds and summarized by Groq—Python, Streamlit, feedparser.

Python Streamlit Groq API feedparser RSS

View on GitHub

Personalized News Digest — AI-curated daily news from live RSS feeds

Personalized News Digest is an intermediate-level project that pulls live news from RSS feeds, summarizes it in your preferred reading style, and delivers it as a coherent daily digest. Select topics, pick a tone, and click once to get “what matters today” without opening eight different news tabs.

The app fetches articles using feedparser, assembles the raw headlines and summaries into a structured prompt, and sends them to Groq for synthesis. Four reading tones are available: beginner-friendly (avoids jargon), technical (precise and data-rich), executive brief (impact-only), and casual (like a friend explaining the news). A final “Key Takeaway” section surfaces cross-topic insights.

Built with Python, Streamlit, feedparser, and the Groq API, this project shows how to combine an external data source with an LLM to produce a personalized, daily-use tool.

Purpose: practice combining live external data with LLM summarization — a pattern used in every news AI, monitoring tool, and executive briefing product.

Typical use: select Technology and AI topics, choose “Executive Brief,” hit Generate, download the digest to read during your commute.

Key ideas you will touch:

  • RSS ingestionfeedparser.parse() fetches and parses live feeds without an API key.
  • Tone injection — four tone strings are appended to the system message; the model adapts vocabulary and depth without any branching code.
  • Source attribution — original article links are listed below the digest so readers can follow up.
  • Custom topic fallback — a free-text field adds a “General Knowledge” section clearly labeled as not from a live feed.

Overall flow

User selects topics + tone + story count in sidebar
      ↓
Clicks "Generate My Digest"
      ↓
feedparser fetches RSS feeds for each selected topic
      ↓
Headlines + summaries assembled into a structured prompt
      ↓
Groq synthesizes into a themed digest with the chosen tone
      ↓
Digest displayed with source links + Download button

Demo


Step-by-Step Implementation

  1. Set up the project
    • Install Python 3.10+, create folder and virtual environment
  2. Install dependencies
    • streamlit, groq, httpx, truststore, feedparser
  3. Define RSS feeds and tones
    • A dict maps topic names to feed URLs; another maps tone names to instruction strings
    • Both dicts drive the UI selectors directly with no separate config file
  4. Build the sidebar preferences
    • st.multiselect for topics, st.selectbox for tone, st.slider for story count
  5. Fetch and parse feeds
    • Loop over selected topics, call feedparser.parse(url)
    • Extract title, summary, and link for each entry; cap summary at MAX_ARTICLE_CHARS; handle fetch errors silently
  6. Build the digest prompt
    • System message: inject tone instruction, specify per-story format (title — 2-3 sentence summary), and request a cross-topic Key Takeaway
    • User message: assemble all articles grouped by topic header
  7. Display and download
    • Render digest with st.markdown(); list source links below; offer st.download_button

Code implementation

python

# Personalized News Digest Generator — Streamlit + Groq + feedparser
# Save as app.py, add GROQ_API_KEY to .streamlit/secrets.toml, then: streamlit run app.py

import ssl
from datetime import datetime

import feedparser
import httpx
import streamlit as st
import truststore

MAX_ARTICLES = 8
MAX_ARTICLE_CHARS = 1200

st.set_page_config(page_title="Personalized News Digest", layout="centered")
st.title("Personalized News Digest")
st.caption(
    "Pick your topics, choose a reading style, and get a curated digest of today's "
    "top stories — summarized in the tone that works for you."
)

_ssl_ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)


@st.cache_resource
def _groq_client():
    import groq

    return groq.Groq(
        api_key=st.secrets["GROQ_API_KEY"],
        http_client=httpx.Client(verify=_ssl_ctx),
    )


_MODEL = "llama-3.3-70b-versatile"

RSS_FEEDS = {
    "Technology": "https://feeds.feedburner.com/TechCrunch",
    "AI & Machine Learning": "https://rss.beehiiv.com/feeds/mTCMoB7KJO.xml",
    "Science": "https://www.sciencedaily.com/rss/top.xml",
    "Business": "https://feeds.bloomberg.com/markets/news.rss",
    "World News": "https://feeds.bbci.co.uk/news/world/rss.xml",
    "Health": "https://feeds.webmd.com/rss/rss.aspx?RSSSource=RSS_PUBLIC",
    "Finance": "https://www.reddit.com/r/finance/.rss",
    "Programming": "https://www.reddit.com/r/programming/.rss",
}

TONES = {
    "Beginner-Friendly": "Use simple language, avoid jargon, explain any technical terms.",
    "Technical": "Use precise technical language, include relevant details and data.",
    "Executive Brief": "Be extremely concise. Focus on business impact and key decisions only.",
    "Casual": "Write in a conversational, engaging tone like you're explaining to a friend.",
}

# ── Settings ───────────────────────────────────────────────────────────────────
with st.sidebar:
    st.header("Preferences")
    selected_topics = st.multiselect(
        "Topics you care about",
        list(RSS_FEEDS.keys()),
        default=["Technology", "AI & Machine Learning"],
    )
    tone = st.selectbox("Reading style", list(TONES.keys()))
    num_stories = st.slider("Stories per topic", 1, 5, 3)
    custom_topics = st.text_input(
        "Custom topic (optional)",
        placeholder="e.g. climate change, crypto",
    )

generate_btn = st.button("Generate My Digest", type="primary", use_container_width=True)

if generate_btn:
    if not selected_topics and not custom_topics.strip():
        st.warning("Select at least one topic.")
        st.stop()

    # ── Fetch RSS articles ─────────────────────────────────────────────────────
    articles_by_topic: dict[str, list[dict]] = {}

    with st.spinner("Fetching latest news…"):
        for topic in selected_topics:
            feed_url = RSS_FEEDS[topic]
            try:
                feed = feedparser.parse(feed_url)
                entries = feed.entries[:num_stories]
                items = []
                for entry in entries:
                    title = entry.get("title", "No title")
                    summary = entry.get("summary", entry.get("description", ""))[:MAX_ARTICLE_CHARS]
                    link = entry.get("link", "")
                    items.append({"title": title, "summary": summary, "link": link})
                if items:
                    articles_by_topic[topic] = items
            except Exception:
                articles_by_topic[topic] = []

    # ── Build prompt ───────────────────────────────────────────────────────────
    articles_text = ""
    for topic, items in articles_by_topic.items():
        articles_text += f"\n## {topic}\n"
        for i, item in enumerate(items, 1):
            articles_text += f"{i}. {item['title']}\n{item['summary']}\n\n"

    tone_instruction = TONES[tone]
    today = datetime.now().strftime("%B %d, %Y")

    system_msg = (
        f"You are a professional news curator creating a personalized digest for {today}. "
        f"Tone: {tone_instruction}\n\n"
        "For each topic section, write:\n"
        "1. A 1-sentence section intro\n"
        "2. Each story as: **[Story title]** — [2-3 sentence summary focusing on why it matters]\n\n"
        "End with a '## Key Takeaway' section with 2-3 cross-topic insights.\n"
        "Only use information from the provided articles. Do not invent facts."
    )
    user_msg = f"Here are today's articles:\n{articles_text[:10000]}"

    if custom_topics.strip():
        user_msg += f"\n\nAlso include a brief section on: {custom_topics} (based on your training data, clearly labeled as 'General Knowledge')."

    with st.spinner("Generating your digest…"):
        try:
            resp = _groq_client().chat.completions.create(
                model=_MODEL,
                messages=[
                    {"role": "system", "content": system_msg},
                    {"role": "user", "content": user_msg},
                ],
            )
            digest = resp.choices[0].message.content

            st.markdown(f"## Your News Digest — {today}")
            st.markdown(digest)

            st.divider()
            st.caption("Sources")
            for topic, items in articles_by_topic.items():
                for item in items:
                    if item["link"]:
                        st.markdown(f"- [{item['title']}]({item['link']})")

            st.download_button(
                "Download Digest (.txt)",
                data=digest,
                file_name=f"digest_{datetime.now().strftime('%Y%m%d')}.txt",
                mime="text/plain",
            )
        except Exception as exc:
            st.error(f"Error generating digest: {exc}")

Complete code Project link (GitHub)


📖 How the Code Works (Step-by-Step)


1. Feed fetching with feedparser

  • feedparser.parse(url) handles RSS and Atom feeds without manual XML parsing; errors are caught silently so one broken feed does not stop the whole digest.
  • Article summaries are capped at MAX_ARTICLE_CHARS to keep the total prompt within the model’s context window.

2. Tone as a string injection

  • Each tone maps to a one-sentence instruction string. That string is inserted into the system message at generation time — no branching code, no separate prompt files.

3. Source attribution

  • Article links are stored in articles_by_topic and rendered as markdown links after the digest, keeping the generated text clean while still giving the reader a path to the original source.

Tips & Production Considerations

Diversify your RSS sources

Relying on a single feed per topic creates a narrow perspective. Add two or three feeds for each category (e.g., both a mainstream outlet and a niche blog for AI news) so the digest captures different angles. The app deduplicates by article link, so overlapping stories are handled automatically.

Cache digests to avoid redundant API calls

Streamlit reruns the script on every interaction. Without caching, clicking a button regenerates the entire digest. Use st.session_state to store the most recent digest and only regenerate when the user explicitly asks for a refresh or the topic selection changes.

Handle feed outages gracefully

RSS feeds go down, return malformed XML, or change URL without notice. Wrap each feedparser.parse call in a try/except and skip failed feeds rather than crashing the entire digest. Log which feeds failed so you can fix or replace them later.

Schedule daily digests with a cron job

For a truly passive experience, wrap the core logic in a script that runs via cron or a GitHub Action on a schedule. Send the generated digest to your email or Slack channel each morning so you read it without opening the app.

Watch token usage on long article lists

Passing 20+ full article summaries to the LLM can exceed context limits or inflate cost. Cap the number of articles per topic (5-10 is usually enough for a digest) and truncate article descriptions to a reasonable character limit before sending them to the model.


Personalized News Digest FAQ

What is the Personalized News Digest?

The Personalized News Digest is an intermediate Streamlit project that pulls live news from RSS feeds, filters by your topics, and summarizes the day in your chosen reading tone—beginner-friendly, technical, or executive brief—using Python, feedparser, and the Groq API.

Is the Personalized News Digest free to use?

The source code is free and open on GitHub. RSS feeds and feedparser are free. You only pay for Groq API usage, and a full daily digest costs a fraction of a cent on Groq's free tier.

What tech stack does the Personalized News Digest use?

Python and Streamlit for the UI, feedparser to read RSS feeds, and the Groq Python client for chat completions. There is no database—the daily digest is generated on demand and cached for the current Streamlit session.

How does the Personalized News Digest pick which stories to summarize?

It fetches the latest items from your configured RSS feeds, filters titles and summaries for your selected topics (e.g., AI, climate, markets), and passes the top N matches to Groq with a system prompt that asks for a digest in the chosen tone.

Can I add my own RSS feeds to the Personalized News Digest?

Yes. Feeds live in a plain config list—paste the RSS URL, give it a category, and it appears in the picker. The app handles polling, deduplication by article link, and per-feed error handling.

What are alternatives to the Personalized News Digest?

For RAG-style Q&A over fixed reading material instead of fresh news, see the Notes Q&A Bot. For long-transcript summaries like meetings or lectures, see the Meeting & Lecture Summarizer.

Browse all →