Personalized News Digest: AI-Curated Daily Briefs From Live RSS Feeds
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.
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 ingestion —
feedparser.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
-
Set up the project
- Install Python 3.10+, create folder and virtual environment
-
Install dependencies
-
streamlit,groq,httpx,truststore,feedparser
-
-
Define RSS feeds and tones
-
A
dictmaps topic names to feed URLs; another maps tone names to instruction strings - Both dicts drive the UI selectors directly with no separate config file
-
A
-
Build the sidebar preferences
-
st.multiselectfor topics,st.selectboxfor tone,st.sliderfor story count
-
-
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
- Loop over selected topics, call
-
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
-
Display and download
-
Render digest with
st.markdown(); list source links below; offerst.download_button
-
Render digest with
Code implementation
# 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_CHARSto 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_topicand 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.