""" 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
or
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""" """ cards_html = "\n".join(article_card(i, a) for i, a in enumerate(articles, 1)) return f""" {title}
Weekly Digest

{title}

Curated AI Agent Intelligence
{date}
This week's top stories, breakthroughs, and releases from the world of autonomous AI agents — researched, summarised, and reviewed by your AI newsletter assistant.
{cards_html}
"""