NewsLetter / tools.py
Siggmoid's picture
inital deploy
4bc5112
Raw
History Blame Contribute Delete
7.99 kB
"""
tools.py — Tool library for the Newsletter Agent.
Three core tools used by the LangGraph nodes:
1. web_search() → DuckDuckGo search (no API key needed)
2. fetch_article() → HTTP fetch + BeautifulSoup text extraction
3. generate_html() → Professional HTML newsletter template renderer
These are plain Python functions (not LangChain @tool wrappers) because
LangGraph nodes call them directly inside node functions.
"""
import re
import requests
from bs4 import BeautifulSoup
from duckduckgo_search import DDGS
# ─── Tool 1: Web Search ───────────────────────────────────────────────────────
def web_search(query: str, max_results: int = 5) -> list[dict]:
"""
Search the web via DuckDuckGo — no API key required.
Returns a list of dicts with keys: title, href, body.
Uses timelimit='w' so results are from the past week.
"""
try:
with DDGS() as ddgs:
results = list(
ddgs.text(query, max_results=max_results, timelimit="w")
)
return results
except Exception as exc:
# Fail gracefully so the agent can continue with other queries
return [{"title": "Search unavailable", "href": "", "body": str(exc)}]
# ─── Tool 2: Article Content Fetcher ─────────────────────────────────────────
def fetch_article(url: str, max_chars: int = 3_000) -> str:
"""
Fetch a URL and return clean plain text up to `max_chars`.
Strategy:
1. Prefer <article> or <main> tags (most article sites use them)
2. Strip scripts / styles / nav / footer noise
3. Collapse whitespace
"""
if not url or not url.startswith("http"):
return "Invalid or missing URL — skipped."
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0 Safari/537.36"
)
}
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.content, "html.parser")
# Remove noise elements
for tag in soup(["script", "style", "nav", "footer",
"header", "aside", "iframe", "noscript"]):
tag.decompose()
# Prefer semantic containers
main = (
soup.find("article")
or soup.find("main")
or soup.find("div", class_=re.compile(r"content|article|post", re.I))
or soup.body
or soup
)
text = main.get_text(separator=" ", strip=True)
text = re.sub(r"\s+", " ", text) # collapse whitespace
return text[:max_chars]
except Exception as exc:
return f"Fetch failed: {exc}"
# ─── Tool 3: HTML Newsletter Generator ───────────────────────────────────────
def generate_html(title: str, date: str, articles: list[dict]) -> str:
"""
Render a professional, self-contained HTML newsletter.
Each item in `articles` should have keys: title, url, summary.
The output is a single HTML string — ready to save as .html or send as email.
"""
def article_card(idx: int, art: dict) -> str:
return f"""
<div class="card">
<div class="card-num">#{idx:02d}</div>
<h2 class="card-title">
<a href="{art.get('url','#')}" target="_blank" rel="noopener">
{art.get('title','Untitled')}
</a>
</h2>
<p class="card-body">{art.get('summary','')}</p>
<a class="card-link" href="{art.get('url','#')}" target="_blank" rel="noopener">
Read full article →
</a>
</div>"""
cards_html = "\n".join(article_card(i, a) for i, a in enumerate(articles, 1))
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<style>
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
font-family: 'Segoe UI', Arial, sans-serif;
background: #f0f2f5;
color: #2d2d2d;
line-height: 1.65;
}}
.wrapper {{ max-width: 680px; margin: 32px auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 32px rgba(0,0,0,.12); }}
/* Header */
.header {{
background: linear-gradient(135deg, #0f0c29 0%, #302b63 55%, #24243e 100%);
padding: 44px 36px 36px;
text-align: center;
color: #fff;
}}
.header .badge {{
display: inline-block;
background: rgba(255,255,255,.15);
border: 1px solid rgba(255,255,255,.25);
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 2px;
padding: 4px 14px;
margin-bottom: 18px;
text-transform: uppercase;
}}
.header h1 {{
font-size: 30px;
font-weight: 800;
letter-spacing: 3px;
text-transform: uppercase;
margin-bottom: 6px;
}}
.header .sub {{ opacity: .65; font-size: 13px; letter-spacing: 1.5px; }}
.header .date {{ margin-top: 14px; font-size: 13px; opacity: .5; }}
/* Intro banner */
.intro {{
background: #eef4ff;
border-left: 5px solid #302b63;
padding: 20px 28px;
font-size: 14.5px;
color: #444;
}}
/* Content area */
.content {{ padding: 28px 28px 8px; }}
.section-label {{
font-size: 11px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: #999;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
margin-bottom: 20px;
}}
/* Article cards */
.card {{
border: 1px solid #e8e8e8;
border-radius: 10px;
padding: 22px 22px 18px;
margin-bottom: 16px;
transition: box-shadow .2s, border-color .2s;
}}
.card:hover {{
box-shadow: 0 6px 20px rgba(48,43,99,.1);
border-color: #c5bef7;
}}
.card-num {{
font-size: 10px;
font-weight: 800;
color: #302b63;
letter-spacing: 1.5px;
margin-bottom: 8px;
text-transform: uppercase;
}}
.card-title {{
font-size: 17px;
font-weight: 700;
margin-bottom: 10px;
line-height: 1.3;
}}
.card-title a {{ color: #1a1a2e; text-decoration: none; }}
.card-title a:hover {{ color: #302b63; text-decoration: underline; }}
.card-body {{ font-size: 14px; color: #555; margin-bottom: 14px; }}
.card-link {{
font-size: 13px;
font-weight: 600;
color: #302b63;
text-decoration: none;
}}
.card-link:hover {{ text-decoration: underline; }}
/* Footer */
.footer {{
background: #1a1a2e;
color: rgba(255,255,255,.55);
text-align: center;
font-size: 12px;
padding: 24px 28px;
margin-top: 12px;
}}
.footer a {{ color: rgba(255,255,255,.4); }}
</style>
</head>
<body>
<div class="wrapper">
<div class="header">
<div class="badge">Weekly Digest</div>
<h1>{title}</h1>
<div class="sub">Curated AI Agent Intelligence</div>
<div class="date">{date}</div>
</div>
<div class="intro">
This week's top stories, breakthroughs, and releases from the world of autonomous AI agents —
researched, summarised, and reviewed by your AI newsletter assistant.
</div>
<div class="content">
<div class="section-label">Top Stories This Week</div>
{cards_html}
</div>
<div class="footer">
<p>Generated by <strong>Newsletter Agent</strong> · Powered by LangGraph</p>
</div>
</div>
</body>
</html>"""