diff --git a/README.md b/README.md index 07fbe23..cb49da2 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ innovation-lab-examples/ | Example | Description | Tech Stack | Difficulty | |---------|-------------|------------|------------| | [contributors/community_agent](contributors/community_agent/) | AI community growth agent for events and hackathons | Python, uAgents, ASI:One, Tavily | 🟡 Intermediate | +| [contributors/news-summarizer-agent](contributors/news-summarizer-agent/) | Fetches top headlines for a topic via NewsAPI and summarizes them with ASI:One, via Chat Protocol | Python, uAgents, NewsAPI, ASI:One | 🟡 Intermediate | ### 🌐 Web3 & Blockchain diff --git a/contributors/CHANGELOG.md b/contributors/CHANGELOG.md index 1c95711..ef54087 100644 --- a/contributors/CHANGELOG.md +++ b/contributors/CHANGELOG.md @@ -7,6 +7,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Added + - `gemini-research-agent/`: Added Gemini-powered research assistant demonstrating the standard Agent Chat Protocol (@Kavurubuvanesh) - `contributors/` folder and contribution guide for community agent examples -- `contributors/community_agent/` — moved from repository root; AI community growth agent for events and hackathons \ No newline at end of file +- `contributors/community_agent/` — moved from repository root; AI community growth agent for events and hackathons +- `contributors/news-summarizer-agent/` — beginner-friendly agent that fetches top headlines via NewsAPI and summarizes them with ASI:One; now a uAgent with Chat Protocol support diff --git a/contributors/news-summarizer-agent/.env.example b/contributors/news-summarizer-agent/.env.example new file mode 100644 index 0000000..5f02f4e --- /dev/null +++ b/contributors/news-summarizer-agent/.env.example @@ -0,0 +1,5 @@ +# Required: ASI:One API key for summarization (https://asi1.ai/) +ASI1_API_KEY=your_asi1_api_key_here + +# Required: NewsAPI key, free tier (https://newsapi.org/register) +NEWS_API_KEY=your_news_api_key_here diff --git a/contributors/news-summarizer-agent/README.md b/contributors/news-summarizer-agent/README.md new file mode 100644 index 0000000..1da57dc --- /dev/null +++ b/contributors/news-summarizer-agent/README.md @@ -0,0 +1,115 @@ +# News Summarizer Agent + +![uAgents](https://img.shields.io/badge/uAgents-chat--protocol-blue) +![News](https://img.shields.io/badge/news-NewsAPI-orange) +![LLM](https://img.shields.io/badge/LLM-ASI%3AOne-green) +![Python](https://img.shields.io/badge/python-3.10+-blue) + +A beginner-friendly Fetch.ai agent that fetches the top news headlines for any +topic using [NewsAPI](https://newsapi.org/) and summarizes them with the +[ASI:One](https://asi1.ai/) LLM. The agent speaks the +[Chat Protocol](https://innovationlab.fetch.ai/resources/docs/agent-communication/agent-chat-protocol), +so it can be messaged directly from ASI:One or any other Agentverse-connected +agent. + +## What it does + +1. You send a topic (e.g. "AI", "climate", "sports") as a chat message. +2. The agent fetches the top 5 latest headlines for that topic from NewsAPI. +3. The headlines are sent to ASI:One for a short summary. +4. The agent replies with the headlines plus a 3-4 sentence summary. + +## Tech stack + +| Layer | Technology | +|-------|------------| +| Agent runtime | [uAgents](https://docs.fetch.ai/agents/uaagents/) + Chat Protocol | +| News data | [NewsAPI](https://newsapi.org/) free tier | +| LLM | [ASI:One](https://asi1.ai/) (`asi1-mini`) | +| HTTP client | `requests` | +| Language | Python 3.10+ | + +## Prerequisites + +- Python 3.10+ +- A free [NewsAPI](https://newsapi.org/register) key +- An [ASI:One](https://asi1.ai/) API key + +## Setup + +### 1. Navigate to this folder + +```bash +cd contributors/news-summarizer-agent +``` + +### 2. Install dependencies + +```bash +pip install -r requirements.txt +``` + +### 3. Set up environment variables + +```bash +cp .env.example .env +``` + +Edit `.env` and fill in your API keys: + +```env +ASI1_API_KEY=your_asi1_api_key_here +NEWS_API_KEY=your_news_api_key_here +``` + +### 4. Run the agent + +```bash +python agent.py +``` + +The agent starts and prints its address. Send it a Chat Protocol message +containing a topic such as `AI`, `climate`, or `sports` and it will reply +with the latest headlines and a summary. + +## Example interaction + +```text +You: sports +Agent: Top headlines for 'sports': + - Headline 1... + - Headline 2... + + Summary: + <3-4 sentence summary generated by ASI:One> +``` + +## Demo + +![Demo output](demo.png) + +## Environment variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `ASI1_API_KEY` | Yes | Your ASI:One API key from [asi1.ai](https://asi1.ai/) | +| `NEWS_API_KEY` | Yes | Your NewsAPI key from [newsapi.org](https://newsapi.org/) | + +## Project structure + +contributors/news-summarizer-agent/ + +├── agent.py # uAgent with Chat Protocol, NewsAPI + ASI:One logic + +├── requirements.txt # Python dependencies + +├── .env.example # Environment variable template + +├── demo.png # Demo screenshot + +└── README.md # This file + + +## License + +MIT (see repository root [LICENSE](../../LICENSE)). diff --git a/contributors/news-summarizer-agent/agent.py b/contributors/news-summarizer-agent/agent.py new file mode 100644 index 0000000..86dfac5 --- /dev/null +++ b/contributors/news-summarizer-agent/agent.py @@ -0,0 +1,169 @@ +""" +News Summarizer Agent + +A beginner-friendly Fetch.ai uAgent that fetches the latest news headlines +for a topic using NewsAPI and summarizes them with the ASI:One LLM. +Supports the Chat Protocol so it can be used directly from ASI:One +or any other Agentverse-connected agent. +""" + +import os +from datetime import datetime, timezone +from uuid import uuid4 + +import requests +from dotenv import load_dotenv +from uagents import Agent, Context, Protocol +from uagents_core.contrib.protocols.chat import ( + ChatAcknowledgement, + ChatMessage, + TextContent, + chat_protocol_spec, +) + +load_dotenv() + +ASI1_API_KEY = os.getenv("ASI1_API_KEY", "") +NEWS_API_KEY = os.getenv("NEWS_API_KEY", "") + +NEWS_API_URL = "https://newsapi.org/v2/everything" +ASI1_API_URL = "https://api.asi1.ai/v1/chat/completions" + + +def fetch_headlines(topic: str) -> list[str]: + """Fetch the top 5 news headlines for a given topic using NewsAPI.""" + params = { + "q": topic, + "pageSize": 5, + "sortBy": "publishedAt", + "language": "en", + "apiKey": NEWS_API_KEY, + } + response = requests.get(NEWS_API_URL, params=params, timeout=15) + response.raise_for_status() + articles = response.json().get("articles", []) + return [a["title"] for a in articles if a.get("title")] + + +def summarize_with_asi1(topic: str, headlines: list[str]) -> str: + """Send headlines to the ASI:One LLM and return a readable summary.""" + headlines_text = "\n".join(f"- {h}" for h in headlines) + prompt = ( + f"Here are the top {len(headlines)} recent news headlines about " + f"'{topic}':\n\n{headlines_text}\n\n" + f"Please write a short, clear, 3-4 sentence summary of what is " + f"currently happening with '{topic}' based on these headlines." + ) + + headers = { + "Authorization": f"Bearer {ASI1_API_KEY}", + "Content-Type": "application/json", + } + payload = { + "model": "asi1-mini", + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.7, + "max_tokens": 300, + "stream": False, + } + + response = requests.post(ASI1_API_URL, headers=headers, json=payload, timeout=30) + response.raise_for_status() + data = response.json() + return str(data["choices"][0]["message"]["content"]) + + +def build_news_summary(topic: str) -> str: + """Fetch headlines for a topic and return a formatted summary string.""" + topic = topic.strip() + if not topic: + return "Please tell me a topic to summarize, e.g. 'AI', 'climate', or 'sports'." + + if not NEWS_API_KEY: + return ( + "This agent is missing a NEWS_API_KEY — ask the operator to configure it." + ) + if not ASI1_API_KEY: + return ( + "This agent is missing an ASI1_API_KEY — ask the operator to configure it." + ) + + headlines = fetch_headlines(topic) + if not headlines: + return f"No recent headlines found for '{topic}'. Try a different topic." + + summary = summarize_with_asi1(topic, headlines) + headlines_block = "\n".join(f"- {h}" for h in headlines) + return f"Top headlines for '{topic}':\n{headlines_block}\n\nSummary:\n{summary}" + + +def run_cli(topic: str) -> None: + """Run the agent's summary flow once and print the result (for local testing).""" + print(f"\nFetching top headlines for topic: '{topic}'...") + print(build_news_summary(topic)) + + +# ─── Chat Protocol ────────────────────────────────────────────── + +chat_proto = Protocol(spec=chat_protocol_spec) + + +@chat_proto.on_message(ChatMessage) +async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage) -> None: + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), + acknowledged_msg_id=msg.msg_id, + ), + ) + + topic = "\n".join( + item.text for item in msg.content if isinstance(item, TextContent) and item.text + ).strip() + + if not topic: + reply = ( + "Hi! Send me a topic (e.g. 'AI', 'climate', or 'sports') and " + "I'll fetch the latest headlines and summarize them for you." + ) + else: + try: + reply = build_news_summary(topic) + except requests.RequestException as exc: + ctx.logger.exception("Upstream API request failed") + reply = f"Sorry, I couldn't fetch news for '{topic}' right now ({exc})." + + await ctx.send( + sender, + ChatMessage( + timestamp=datetime.now(timezone.utc), + msg_id=uuid4(), + content=[TextContent(type="text", text=reply)], + ), + ) + + +@chat_proto.on_message(ChatAcknowledgement) +async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement) -> None: + ctx.logger.info( + f"Received acknowledgement from {sender} for message {msg.acknowledged_msg_id}" + ) + + +# ─── Agent setup ───────────────────────────────────────────────── + +agent = Agent() + +agent.include(chat_proto, publish_manifest=True) + + +@agent.on_event("startup") +async def on_startup(ctx: Context) -> None: + ctx.logger.info(f"News Summarizer Agent started at address {agent.address}") + ctx.logger.info(f"NEWS_API_KEY configured: {bool(NEWS_API_KEY)}") + ctx.logger.info(f"ASI1_API_KEY configured: {bool(ASI1_API_KEY)}") + + +if __name__ == "__main__": + agent.run() diff --git a/contributors/news-summarizer-agent/demo.png b/contributors/news-summarizer-agent/demo.png new file mode 100644 index 0000000..ffeffa6 Binary files /dev/null and b/contributors/news-summarizer-agent/demo.png differ diff --git a/contributors/news-summarizer-agent/requirements.txt b/contributors/news-summarizer-agent/requirements.txt new file mode 100644 index 0000000..ba7e110 --- /dev/null +++ b/contributors/news-summarizer-agent/requirements.txt @@ -0,0 +1,3 @@ +uagents>=0.20.0 +requests>=2.28.0 +python-dotenv>=1.0.0