All articles

Automation7 min read

Automated Newsletter Summarizer with Gemini Search Grounding and Sentiment Analysis

A configurable topic-based news pipeline that retrieves current articles, generates sourced summaries with bullish/bearish signals, and outputs newsletter-ready Markdown — on Cloud Scheduler

  • Gemini
  • Vertex AI
  • Google Search Grounding
  • Cloud Scheduler
  • Cloud Run
  • Python

The information problem for technical teams is volume, not access: there's too much relevant news to track manually, and most AI-generated summaries hallucinate sources or pull from knowledge cutoffs that are months old. Google Search grounding in Vertex AI solves the cutoff problem — Gemini retrieves current articles before generating, and cites them explicitly in the output.

Pipeline Architecture

Config: topics.json (list of topic seeds)
  ↓
For each topic (parallel):
  Gemini 1.5 Pro + Vertex AI Search Grounding
    ├── Retrieves 5–10 current articles via Google Search
    ├── Extracts: key facts, dates, entities, source URLs
    ├── Generates 2–3 sentence summary with attribution
    └── Assigns sentiment: bullish | bearish | neutral | mixed
  ↓
Synthesis pass (Gemini)
  ├── Cross-topic patterns ("AI + energy sector converging")
  └── Anomaly flags ("unusual coverage spike on topic X")
  ↓
Newsletter Assembly
  ├── Per-topic sections (Markdown H2)
  ├── Executive summary: top 3 developments
  └── Full source list (all URLs cited)
  ↓
Outputs: CSV + JSON + Markdown
  → GCS archive + optional SMTP delivery

Gemini with Search Grounding

The key is using Vertex AI's grounded generation — Gemini retrieves current web content before generating, so summaries reflect what was published this week, not what was in training data:

python
1import vertexai
2from vertexai.preview.generative_models import (
3    GenerativeModel,
4    Tool,
5    grounding,
6)
7
8vertexai.init(project=PROJECT_ID, location="us-central1")
9model = GenerativeModel("gemini-1.5-pro-002")
10
11# Google Search grounding tool
12search_tool = Tool.from_google_search_retrieval(
13    google_search_retrieval=grounding.GoogleSearchRetrieval()
14)
15
16def summarize_topic(topic: str) -> dict:
17    prompt = f"""You are a research analyst writing a newsletter section.
18
19Topic: {topic}
20
21Search for and summarize the most important news and developments on this topic
22from the past 7 days. For each key development:
231. State the fact clearly (who, what, when)
242. Cite the source (publication + URL)
253. Explain the significance
26
27After the summary, assign an overall sentiment:
28BULLISH if developments are positive/growth-indicating
29BEARISH if developments are negative/risk-indicating
30NEUTRAL if factual/informational
31MIXED if both positive and negative
32
33Format your response as:
34SUMMARY: [2-3 sentence narrative]
35SENTIMENT: [BULLISH|BEARISH|NEUTRAL|MIXED]
36REASON: [1 sentence explaining sentiment choice]
37SOURCES: [list of "title — url" pairs]
38"""
39    response = model.generate_content(
40        prompt,
41        tools=[search_tool],
42        generation_config={"temperature": 0.2},  # Lower temp for factual accuracy
43    )
44    return parse_response(response.text)

Topic Configuration

python
1# config/topics.json — customize per client
2{
3  "topics": [
4    "artificial intelligence retail applications 2025",
5    "Google Cloud BigQuery announcements",
6    "energy grid forecasting machine learning Romania",
7    "Gemini API updates and new features",
8    "EU AI Act compliance enterprise"
9  ],
10  "output_format": "markdown",
11  "max_articles_per_topic": 5,
12  "language": "en",
13  "sentiment_analysis": true,
14  "delivery": {
15    "gcs_bucket": "gs://formare-ai-newsletters",
16    "smtp_to": ["team@example.com"],
17    "schedule": "daily"
18  }
19}

Newsletter Assembly

python
1from datetime import datetime
2
3SENTIMENT_EMOJI = {
4    "BULLISH": "📈",
5    "BEARISH": "📉",
6    "NEUTRAL": "📊",
7    "MIXED": "⚖️",
8}
9
10def assemble_newsletter(summaries: list[dict], week_of: datetime) -> str:
11    lines = [
12        f"# Weekly AI & Tech Briefing — {week_of.strftime('%B %d, %Y')}",
13        "",
14        "## Executive Summary",
15        "",
16        *[f"- {s['key_takeaway']}" for s in sorted(
17            summaries, key=lambda x: x.get("significance_score", 0), reverse=True
18        )[:3]],
19        "",
20    ]
21
22    for summary in summaries:
23        sentiment = summary.get("sentiment", "NEUTRAL")
24        emoji = SENTIMENT_EMOJI.get(sentiment, "")
25        lines.extend([
26            f"## {summary['topic']} {emoji}",
27            "",
28            summary["summary_text"],
29            "",
30            f"**Sentiment:** {sentiment} — {summary.get('sentiment_reason', '')}",
31            "",
32            "**Sources:**",
33            *[f"- [{s['title']}]({s['url']})" for s in summary.get("sources", [])],
34            "",
35            "---",
36            "",
37        ])
38
39    return "\n".join(lines)

Cloud Scheduler Deployment

bash
1# Deploy as Cloud Run Job
2gcloud run jobs create newsletter-summarizer \
3  --image europe-west1-docker.pkg.dev/$PROJECT/newsletter/summarizer:latest \
4  --region europe-west1 \
5  --service-account newsletter-sa@$PROJECT.iam.gserviceaccount.com \
6  --set-env-vars PROJECT_ID=$PROJECT \
7  --set-secrets CONFIG_PATH=newsletter-config:latest
8
9# Schedule daily at 07:00 CET
10gcloud scheduler jobs create http newsletter-daily \
11  --schedule="0 7 * * *" \
12  --uri="https://europe-west1-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/$PROJECT/jobs/newsletter-summarizer:run" \
13  --http-method=POST \
14  --oauth-service-account-email=newsletter-sa@$PROJECT.iam.gserviceaccount.com \
15  --time-zone="Europe/Bucharest"
News from Google Search (no knowledge cutoff)
Current
Sentiment signals (bullish/bearish/neutral/mixed)
4
Cloud Scheduler runs
Daily
Output formats (CSV + JSON + Markdown)
3

Request a discovery call

30 minutesGoogle MeetFree