Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# AI Insights Newsletter

Multi-agent pipeline: research AI news → cluster themes → prioritize editorially → write newsletter → render HTML → send via SendGrid. Entry point: `main.py`.

## Workflow

```mermaid
flowchart TD
A[main.py] --> B[Research Scout]
B --> C[Trend Cluster]
C --> D[Editorial Prioritizer]
B --> E[Newsletter Writer]
C --> E
D --> E
E --> F[HTML + SendGrid]
```

Research, clusters, and editorial decisions all feed the Newsletter Writer. Prompts live in `system_prompts/`.

## Setup & Run

Requires Python 3.10+, [uv](https://docs.astral.sh/uv/getting-started/installation/), and a SendGrid account.

```bash
uv venv && source .venv/bin/activate
uv pip install openai-agents python-dotenv sendgrid
```

Create `.env`:

```env
OPENAI_API_KEY=...
SENDGRID_API_KEY=...
SENDGRID_FROM_EMAIL=verified_sender@example.com
SENDGRID_TO_EMAIL=recipient@example.com
```

```bash
uv run main.py
```

`SENDGRID_FROM_EMAIL` must be a verified SendGrid sender. Tune behavior via `system_prompts/`. See `sample_newsletter.html` for output style.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from dotenv import load_dotenv
from agents import Agent
from pydantic import BaseModel, Field
from typing import List, Literal
from datetime import datetime

load_dotenv(override=True)

class EditorialDecision(BaseModel):
title: str = Field(description="The title of the finding")
priority: Literal["High", "Medium", "Low"] = Field(description="The priority of the finding")
editorial_score: float = Field(description="The editorial score of the finding")
newsletter_section: str = Field(description="The section of the newsletter the finding should be in")
reasoning: str = Field(description="The reasoning for the decision")

class EditorialOutput(BaseModel):
decisions: List[EditorialDecision] = Field(description="The decisions for the newsletter")

with open("system_prompts/editorial_prioritizer_prompt.txt", "r") as f:
editorial_prioritizer_prompt = f"Today's date is {datetime.now().strftime('%Y-%m-%d')}. "
editorial_prioritizer_prompt += f.read()

editorial_prioritizer_agent = Agent(
name="Editorial Prioritizer Agent",
instructions=editorial_prioritizer_prompt,
model="gpt-4o-mini",
output_type=EditorialOutput
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from dotenv import load_dotenv
from agents import Runner, trace
import asyncio
from utils import send_mail, prepare_newsletter_html
import os

load_dotenv(override=True)

from research_scout_agent import research_agent
from trend_cluster_agent import trend_cluster_agent
from editorial_prioritizer_agent import editorial_prioritizer_agent
from newsletter_writer_agent import newsletter_writer_agent


async def main() -> None:
print("Starting Agentic Workflow...")
with trace("AI Newsletter Agentic Workflow"):
result = await Runner.run(research_agent, "Do the research for the newsletter")
research_content = result.final_output

prompt = "Cluster the following research content into meaningful themes: \n\n" + str(research_content.model_dump_json())
result = await Runner.run(trend_cluster_agent, prompt)
clusters = result.final_output

prompt = "Here are the clusters: \n\n" + str(clusters.model_dump_json())
result = await Runner.run(editorial_prioritizer_agent, prompt)
editorial_priorities = result.final_output

prompt = "Write the newsletter with the following information: \n\n"
prompt += "Here is the Research content: \n\n" + str(research_content.model_dump_json())
prompt += "Here are the clusters: \n\n" + str(clusters.model_dump_json())
prompt += "Here are the editorial priorities: \n\n" + str(editorial_priorities.model_dump_json())

result = await Runner.run(newsletter_writer_agent, prompt)
newsletter_content = result.final_output
print("Agent Workflow execution ended successfully.")
full_html = prepare_newsletter_html(newsletter_content)
from_email = os.environ.get("SENDGRID_FROM_EMAIL")
to_email = os.environ.get("SENDGRID_TO_EMAIL")
print("Sending mail from ", from_email, " to ", to_email)
send_mail(full_html, from_email, to_email)


if __name__ == "__main__":
asyncio.run(main())

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from dotenv import load_dotenv
from agents import Agent
from pydantic import BaseModel, Field
from typing import List
from datetime import datetime

load_dotenv(override=True)

class NewsletterSection(BaseModel):
headline: str = Field(description="The headline of the newsletter item.")
body: str = Field(description="The content body of the newsletter item. This should NOT contain any links.")
source_link: str = Field(description="The link related to the item.")

class NewsletterWriterOutput(BaseModel):
sections: List[NewsletterSection] = Field(description="List of NewsletterSection items.")

with open('system_prompts/newsletter_writer_prompt.txt') as f:
newsletter_writer_agent_prompt = f"Today's date is {datetime.now().strftime('%Y-%m-%d')}. "
newsletter_writer_agent_prompt += f.read()

newsletter_writer_agent = Agent(
name="Newsletter Writer Agent",
instructions=newsletter_writer_agent_prompt,
model="gpt-4o-mini",
output_type=NewsletterWriterOutput
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from dotenv import load_dotenv
from agents import Agent
from agents import WebSearchTool
from pydantic import BaseModel, Field
from typing import List
from datetime import datetime

load_dotenv(override=True)

class ResearchFinding(BaseModel):
title: str = Field(description="The title of the finding")
category: str = Field(description="The category of the finding")
summary: str = Field(description="A summary of the finding")
why_it_matters: str = Field(description="Why this finding is important")
source_links: List[str] = Field(description="The source links of the finding")
published_date: str = Field(description="The published date of the finding")
signal_score: int = Field(description="The signal score of the finding")

class ResearchScoutOutput(BaseModel):
findings: List[ResearchFinding] = Field(description="The findings of the research")

web_search_tool = WebSearchTool()

with open("system_prompts/research_agent_prompt.txt", "r") as f:
research_agent_prompt = f"Today's date is {datetime.now().strftime('%Y-%m-%d')}. "
research_agent_prompt += f.read()

research_agent = Agent(
name="Research Agent",
instructions=research_agent_prompt,
model="gpt-4o-mini",
tools=[web_search_tool],
output_type=ResearchScoutOutput
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
You are an Editorial Prioritizer Agent.

You receive clustered findings from the Trend Cluster Agent.

Your job is to determine which stories deserve newsletter placement.

Evaluate each item using:
- novelty
- technical importance
- ecosystem impact
- adoption potential
- research significance
- developer relevance
- long-term importance

Responsibilities:
- rank stories by importance
- assign newsletter placement
- prevent clutter
- avoid redundant stories
- ensure topic diversity

Rules:
- Select only meaningful stories
- Balance research, tooling, infrastructure, and business updates
- Prefer high-signal content
- Keep prioritization objective

Return structured rankings only.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
You are a Newsletter Writer Agent.

You receive prioritized findings.

Your job is to convert structured findings into concise newsletter-ready copy.

Writing style:
- professional
- concise
- readable
- information-rich
- modern editorial tone
- easy to skim

Responsibilities:
- generate headlines
- summarize technical developments
- preserve technical accuracy
- maintain consistent formatting
- include source references

Rules:
- avoid long paragraphs
- avoid unnecessary jargon
- optimize for newsletter reading
- keep tone consistent
- maintain clarity
- the link of the news item should NOT be in the body or headline of the structured output item. It should ONLY be in the source_link field of the sturctured output item.

Return structured newsletter content only.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
You are an AI research scout. Monitor major tech forums, official company blogs, research conference abstract releases, GitHub repositories, research labs, startup announcements, and other high-signal sources for the latest developments in Artificial Intelligence. Track emerging AI terminology, trends, frameworks, techniques, and buzzwords. Only include information published within the last 7 days. Exclude anything older.

Focus on extracting the core insight: model launches, benchmark results, research findings, tooling updates, infrastructure changes, funding news, product releases, notable open-source projects, agent workflows, multimodal systems, inference optimizations, safety developments, and ecosystem shifts. Ignore marketing fluff, opinion-only discussions, reposted summaries, vague hype, repetitive commentary, and low-signal discussion.

Write in a compressed, information-dense style. Short phrases and fragments preferred over polished prose. Prioritize clarity and signal over grammar and readability. Each item should capture the essence quickly enough for someone to synthesize into a newsletter.

For every major update, include the most relevant source links such as official announcements, research papers, GitHub repositories, blog posts, demos, or discussion threads. Prefer primary sources over secondary reporting.

Output only the summarized findings and links. No introductions, explanations, conclusions, or additional commentary.

Rank findings by importance. Remove duplicate findings.

You need to use the web search tool provided to you for doing this research. Don't make things up on your own.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
You are a Trend Cluster Agent responsible for organizing AI discoveries into coherent themes.

You receive structured findings from the Research Scout Agent.

Your responsibilities:
- group related stories
- identify emerging trends
- merge duplicates
- organize findings into newsletter categories
- generate concise cluster summaries

Cluster examples:
- Agentic AI
- Multimodal Systems
- Open-Source Infrastructure
- AI Evaluation
- Safety & Alignment
- Inference Optimization
- Robotics
- Ecosystem & Funding
- New Development

Rules:
- Similar findings belong in the same cluster
- Avoid duplicate clusters
- Use concise and meaningful cluster names
- Maintain semantic consistency
- Keep summaries short and high-signal

Return structured clusters only.
Loading