Daily News Briefing
Every morning this app pulls the day's news from several sources, has an LLM curate it and rewrite it into a spoken briefing, and then phones me. A conversational voice agent reads the briefing and answers follow-up questions about any story. It runs on Google Cloud, so it doesn't depend on my laptop.
The briefing has four segments, all configured in feeds.yaml:
| Segment | Sources |
|---|---|
| AI & Tech | TLDR AI (tldr.tech), Hacker News, Ars Technica / The Verge / TechCrunch RSS |
| US Headlines | NPR, CBS, ABC, The Hill RSS |
| Immigration & Visas | Guardian + Immigration Impact RSS, plus keyword-filtered politics feeds |
| Markets | S&P 500, Nasdaq, Dow + a watchlist (via yfinance) |
How it works
flowchart TD
Sched[Cloud Scheduler · 11:30 AM ET] --> Job[Cloud Run Job → python -m agent.sync --call]
Job --> Loop{for each feed in feeds.yaml}
Loop --> Fetch[sources/ · fetch + merge stories]
Fetch --> Cur[curation/ · pick + rank]
Cur --> Enr[enrich thin stories with article text]
Enr --> Rew[rewrite · Gemini 2.5 Pro]
Rew --> Loop
Loop -->|all segments| Doc[agent/context.py · narrative + source stories]
Doc --> KB[ElevenLabs agent · knowledge base + anchor persona]
KB --> Call[outbound call via Twilio number → my phone]
- Fetch. Each feed merges stories from its sources into a common
Storyshape. If one source fails, it's skipped and the rest still run. - Curate. A curator ranks and trims the candidates.
agenticis a Google ADK agent that can fetch articles and check past digests;deterministicranks by recency and keywords. If the agentic curator errors, it falls back to deterministic. - Rewrite. Gemini turns each segment into speech-ready prose at temperature 0.2. The prompts
in
prompts/stop it from inventing facts. - Sync + call. The composed narrative and the grounding stories are uploaded as the agent's
knowledge doc for the day. ElevenLabs then rings
USER_PHONE_NUMBER.
The original MP3-by-email path (main.py) still works: it rewrites the briefing, synthesizes
it with Cloud TTS (Chirp3-HD), and emails the clip. It's no longer what production runs, but it's
useful locally.
For the full picture (diagrams, patterns, how to extend), see ARCHITECTURE.md.
Quick start (local)
python -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env # fill in Gmail, GCP project, ElevenLabs keys
gcloud auth application-default login # Vertex AI + TTS credentials
Then run whichever entry point you need:
# Phone-agent path (what production runs)
.venv/bin/python -m agent.sync --dry-run # build + print today's knowledge doc, touch nothing
.venv/bin/python -m agent.sync # refresh the ElevenLabs agent's knowledge
.venv/bin/python -m agent.sync --call # refresh, then ring USER_PHONE_NUMBER
.venv/bin/python -m agent.sync --call-only # ring without rebuilding (telephony test)
# MP3-by-email path
DRY_RUN=1 .venv/bin/python main.py # print the script, no audio/email
.venv/bin/python main.py # full: TTS + email
# Tests
.venv/bin/python -m pytest tests -q
GMAIL_ADDRESS / GMAIL_APP_PASSWORD are only needed for the MP3-by-email path (main.py),
which validates them when it sends; the phone-agent and MCP paths build their config without them.
MCP server
The news engine (fetch → curate → rewrite) is also exposed as an MCP server, so any MCP client (Claude Desktop, Cursor, …) can pull AI/news content. It runs locally and uses your own credentials — nothing is sent to a hosted service. (The voice/phone-call feature is deliberately not exposed over MCP.)
| Tool | Credentials | What it does |
|---|---|---|
list_topics() |
none | the topics covered ("AI & Tech", "US Headlines", …) |
get_stories(topic) |
none | today's raw source stories for a topic |
get_briefing(topic?) |
Google Cloud / Vertex | curated, rewritten briefing (one topic, or all) |
.venv/bin/python mcp_server.py # speaks MCP over stdio
Wire it into Claude Desktop (claude_desktop_config.json) — keys stay on your machine:
{
"mcpServers": {
"daily-news": {
"command": "/absolute/path/to/.venv/bin/python",
"args": ["/absolute/path/to/Daily_News/mcp_server.py"],
"env": { "GOOGLE_CLOUD_PROJECT": "your-gcp-project" } // only needed for get_briefing
}
}
}
list_topics and get_stories need no credentials or extra packages. get_briefing additionally
requires the briefing extra (the Gemini/Vertex stack) plus gcloud auth application-default login:
pip install "daily-news-mcp[briefing]" # adds google-genai + google-adk for get_briefing
Without the extra, get_briefing returns a clear "install the briefing extra" message; the keyless
tools still work. Once published to PyPI, uvx daily-news-mcp runs the server without a local
checkout (uvx "daily-news-mcp[briefing]" to include get_briefing).
Deploy
export USER_PHONE_NUMBER='+1...' # who the agent calls (kept out of the repo)
bash deploy.sh
deploy.sh enables the APIs, builds the image with Cloud Build, stores the Gmail and ElevenLabs
secrets in Secret Manager, grants the vertex-ai-runner@ service account its roles (including
cloudtrace.agent), deploys the Cloud Run Job with its entrypoint overridden to
python -m agent.sync --call, and creates the Scheduler trigger (default 30 11 * * *,
America/New_York).
You also need an ElevenLabs Conversational AI agent, and a Twilio number imported into ElevenLabs. ElevenLabs places the call with that number's credentials, so the app never calls Twilio directly.
gcloud run jobs execute daily-news --region us-central1 # run now (refresh + call)
gcloud run jobs executions list --job daily-news --region us-central1
Configuration
Feeds, sources, word budgets, and story caps live in feeds.yaml. To add a topic, add a feed
there plus a prompt in prompts/; no code changes are needed. Everything else is an environment
variable (see .env.example). The main ones:
| Variable | Purpose | Default |
|---|---|---|
GMAIL_ADDRESS / GMAIL_APP_PASSWORD |
Gmail auth (SMTP for the email path; required everywhere) | — (required) |
GOOGLE_CLOUD_PROJECT |
GCP project for Vertex AI | — |
GEMINI_MODEL |
Curator + judges | gemini-2.5-flash |
GEMINI_REWRITE_MODEL |
Segment rewrite | gemini-2.5-pro |
REWRITE_TEMPERATURE / CURATOR_TEMPERATURE |
Kept low so the model paraphrases instead of inventing | 0.2 / 0.2 |
CURATOR |
agentic (ADK) or deterministic |
agentic (.env.example sets deterministic) |
ENRICH_ARTICLES / ENRICH_MAX_PER_FEED |
Fetch article text for headline-only stories | on / 8 |
REALTIME_PROVIDER |
Voice-agent backend | elevenlabs |
ELEVENLABS_API_KEY / ELEVENLABS_AGENT_ID / ELEVENLABS_PHONE_NUMBER_ID |
The phone agent | — |
USER_PHONE_NUMBER |
Who the agent calls | — |
TTS_VOICE / TTS_MODEL |
Cloud TTS for the MP3 path | en-US-Chirp3-HD-Charon / stable voice |
TRACE_ENABLED / TRACE_BACKEND |
OpenTelemetry → Cloud Trace | on / cloudtrace |
GCS_BUCKET |
Optional archive | (off) |
DRY_RUN |
Print instead of synthesize/send (main.py) |
off |
Evals
evals/ grades segments against a frozen golden set: the day's captured sources, so a score
change reflects a code change rather than different news. There are two kinds of grading.
- Reference-free (no answer key needed): code checks for length, structure, and TTS safety,
plus LLM judges for faithfulness, coverage, and relevance, and ADK's
hallucinations_v1. - Reference-based: graded against a hand-written
expectations.mdanswer key, both as a whole and rule by rule as ADK rubrics.
python -m evals.capture --name YYYY-MM-DD # freeze today's sources
python -m evals.run --golden YYYY-MM-DD --curator agentic # own judges
python -m evals.run --golden YYYY-MM-DD --curator agentic --adk-metrics # + Google's ADK judges
python -m evals.compare --golden YYYY-MM-DD # deterministic vs agentic
Observability
Each run is a single trace in Cloud Trace. ADK emits GenAI spans for the curator's model and tool
calls, raw google-genai calls are auto-instrumented, and observability.py adds spans for the
pipeline's own stages (gather, curate, enrich, rewrite, sync, call), with token counts on every
model call. No LangSmith or LangChain is involved.
To watch the curator agent turn by turn: adk web adk_apps.
Project layout
| Path | Job |
|---|---|
agent/ |
Phone-agent entry point (sync.py), knowledge doc (context.py), providers (ElevenLabs live; Gemini Live / Retell are stubs), anchor persona prompt |
main.py |
MP3-by-email entry point: TTS, email, GCS archive |
pipeline.py |
Gather → curate → enrich → rewrite → compose |
sources/ |
One adapter per source: tldr_web, hackernews, rss, stocks (plus the unused gmail_newsletter and google_news_rss) |
curation/ |
deterministic.py and agentic.py behind a shared Curator interface |
prompts/ |
Per-feed rewrite and curation prompts + _shared_style.md |
composer.py |
Stitches segments into one script |
evals/ |
Capture, checks, judges, ADK metrics, compare, golden sets |
observability.py |
Tracing bootstrap + span helpers |
adk_apps/ |
Dev-only adapter that exposes the curator to adk web |
deploy.sh / Dockerfile |
Ship to Cloud Run |
STATE.md is my running dev log. docs/field-notes.html holds longer notes.
Metadata
Release files for daily-news-mcp 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| daily_news_mcp-0.1.1.tar.gz | 362.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| daily_news_mcp-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 437.4 kB
Release files / daily_news_mcp-0.1.1.tar.gz
| Download URL | daily_news_mcp-0.1.1.tar.gz |
|---|---|
| Size | 362.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fc0afc5c2a37a9a7b6990143cba6fcfbf3f52dc7db7586ee8f40d655aec008b5
|
|
BLAKE2b-256 checksum How to use checksums |
be778471221b649bd484b4ee9f7c7669c0b2b82afd51bb4a334928d9c638cbb2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / daily_news_mcp-0.1.1-py3-none-any.whl
| Download URL | daily_news_mcp-0.1.1-py3-none-any.whl |
|---|---|
| Size | 75.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
28908b4ac0560218101700d2325a1bc9917f3b61a4e6f8148b96105c8a2b34da
|
|
BLAKE2b-256 checksum How to use checksums |
edf2cc30474ad1d106fdd3b531ab49909a049b2aafc326ba2e503053d8e4f074
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|