diff --git a/contributors/CHANGELOG.md b/contributors/CHANGELOG.md new file mode 100644 index 0000000..19aa5be --- /dev/null +++ b/contributors/CHANGELOG.md @@ -0,0 +1,14 @@ +# Contributors Changelog + +All notable community-submitted agent examples are documented in this file. + +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Added + +- `contributors/` folder and contribution guide for community agent examples +- `contributors/community_agent/` — moved from repository root; AI community growth agent for events and hackathons +- `contributors/weather_agent/` – Added a beginner-friendly weather agent using the Open-Meteo API that provides real-time weather information for any city without requiring API keys or paid services. + diff --git a/contributors/README.md b/contributors/README.md new file mode 100644 index 0000000..dff7dd0 --- /dev/null +++ b/contributors/README.md @@ -0,0 +1,109 @@ +# Community Contributors — Agent Examples + +This folder is the **home for all community-submitted agent examples**. If you are contributing a new agent to this repository, create it here — not at the repository root. + +Maintained examples (Fetch.ai team) live in top-level folders such as `fetch-hackathon-quickstarter/`, `gemini-quickstart/`, etc. **Community PRs for new agents must go under `contributors//`.** + +--- + +## Quick start + +1. **Star** the repository (required for CI — see [CONTRIBUTING.md](../CONTRIBUTING.md)). +2. **Fork** and branch from `main`: + ```bash + git checkout -b feat/my-agent-name + ``` +3. **Create your agent folder**: + ```text + contributors// + README.md + requirements.txt + .env.example # if API keys are needed + agent.py # or your entry script + assets/ + demo.png # screenshot or GIF (recommended) + ``` +4. **Copy the README template**: [docs/AGENT_README_TEMPLATE.md](../docs/AGENT_README_TEMPLATE.md) +5. **Add a changelog entry** in [contributors/CHANGELOG.md](./CHANGELOG.md). +6. **Update the root README** — add your agent to the **Community Contributors** table in [README.md](../README.md). +7. **Open a PR** — your PR must pass CI and receive **at least one review** before merge. + +--- + +## What makes a good contributor agent? + +| Area | Guidance | +|------|----------| +| **Purpose** | Solves a clear, real-world task (booking, search, payments, automation, etc.) | +| **Runnable** | Works locally with documented setup; include `.env.example` | +| **Fetch.ai stack** | Prefer **uAgents**, **ASI:One**, **Agentverse**, **A2A**, or **MCP** where relevant | +| **Docs** | Complete README with install, env vars, run steps, and demo | +| **Safety** | No real secrets in code; read-only or sandbox APIs for demos when possible | + +--- + +## Real-time / transactional agents (challenge ideas) + +Looking for inspiration? These are high-value patterns the community is encouraged to build (see open issues): + +- **Flight booking** — search, compare, hold or mock-book via a travel API (e.g. Duffel, Amadeus sandbox) +- **Hotel booking** — availability, pricing, reservation flow with clear mock/sandbox mode +- **Payment gateway** — Stripe or similar with a gated premium step (see `stripe-payment-agents/` for patterns) +- **Event tickets** — live inventory + checkout deep links (see `mcp-agents/ticketlens-agent/`) +- **Multi-step workflows** — planner → payment → confirmation with chat protocol + +Reference examples elsewhere in the repo: + +- [flight-tracker-openai-workflow-agent](../flight-tracker-openai-workflow-agent/) +- [stripe-payment-agents](../stripe-payment-agents/) +- [mcp-agents/ticketlens-agent](../mcp-agents/ticketlens-agent/) +- [contributors/community_agent](./community_agent/) — community growth / events agent + +--- + +## Changelog + +Every non-documentation change under `contributors/` must update [contributors/CHANGELOG.md](./CHANGELOG.md). The root [CHANGELOG.md](../CHANGELOG.md) is updated by maintainers when we cut releases. + +--- + +## CI checks for contributor PRs + +Your pull request must pass: + +| Check | What it does | +|-------|----------------| +| `stargazer-gate` | PR author has starred the repo | +| `contributor-path-check` | New agent folders are only under `contributors/` | +| `changelog-check` | `contributors/CHANGELOG.md` updated when you change code | +| `review-required` | At least one approving review — **skipped for maintainers** | +| `lint` / `format` / `typecheck` | Python quality on changed files | +| `validate-architecture` | Required repo files present | +| `test` | Runs pytest when tests exist | + +**PRs cannot be merged without review approval** when branch protection is enabled (see [.github/BRANCH_PROTECTION.md](../.github/BRANCH_PROTECTION.md)). Maintainers with repo write access skip this check. + +--- + +## Contributor badge (after your PR merges) + +1. Badge is added to your agent `README.md` under `contributors//` +2. You are listed in [BADGE_REGISTRY.json](./BADGE_REGISTRY.json) +3. Install [profile-badge-sync](./profile-badge-sync/README.md) in your `GitHubUsername/GitHubUsername` repo for **automatic** profile README badge, or paste the markdown from the merge comment. + +--- + +## Need help? + +- [CONTRIBUTING.md](../CONTRIBUTING.md) — full contribution policy +- [ISSUES_GUIDE.md](../ISSUES_GUIDE.md) — how to file bugs and pick up tasks +- [Innovation Lab docs](https://innovationlab.fetch.ai/resources/docs/intro) +- Comment on an issue or ask in your PR — maintainers are happy to guide you + +--- + +## Existing community agents + +| Agent | Description | +|-------|-------------| +| [community_agent](./community_agent/) | AI community growth agent for events, conferences, and hackathons | diff --git a/contributors/weather_agent/README.md b/contributors/weather_agent/README.md new file mode 100644 index 0000000..d85afbd --- /dev/null +++ b/contributors/weather_agent/README.md @@ -0,0 +1,203 @@ +# weather-agent + +> 🟢 **Beginner** · A uAgent that fetches real-time weather for any city using the free [Open-Meteo API](https://open-meteo.com/) — **no API key, no sign-up, zero cost**. + +--- + +## 1) Project Title + +`weather-agent` + +A Fetch.ai uAgent that receives a city name and returns live weather conditions (temperature, humidity, wind speed, sky description) by querying the Open-Meteo REST API. + +--- + +## 2) Overview + +Most beginner agent examples either call a mocked API or require the user to sign up for a paid weather service. This example bridges that gap: it connects to a **real external API** the moment you run it, with **zero configuration**. + +The agent listens for `WeatherRequest` messages, geocodes the city name via Open-Meteo's free Geocoding API, fetches current conditions, and replies with a structured `WeatherResponse` message. + +- **Category:** `automation / real-world API` +- **Tech stack:** `Python · uAgents · Open-Meteo REST API · requests` +- **Difficulty:** 🟢 Beginner +- **Status:** `demo` + +--- + +## 3) Features + +- 🌍 Accepts **any city name** in the world (resolved via the Open-Meteo Geocoding API) +- 🌡️ Returns temperature, feels-like temperature, humidity, wind speed, and a WMO sky description +- 🔑 **No API key required** — completely free and open +- 🔁 Structured message passing with typed `Model` schemas shared between agent and client +- 🛡️ Graceful error handling for unknown cities or network failures + +--- + +## 4) Prerequisites + +| Requirement | Version | +|---|---| +| Python | ≥ 3.10 | +| pip | any recent version | +| Internet access | required (calls `open-meteo.com`) | + +No API keys are needed. + +--- + +## 5) Installation + +```bash +git clone https://github.com/innovation-lab-examples.git +cd innovation-lab-examples/weather-agent + +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate + +pip install -r requirements.txt +``` + +--- + +## 6) Environment Variables + +This agent works out of the box with no `.env` required. If you want to customise the agent's identity seed or prepare it for Agentverse, create a `.env` file: + +```bash +cp .env.example .env +``` + +### Variables + +| Variable | Required | Description | +|---|---|---| +| `AGENT_SEED` | ❌ Optional | Seed phrase that determines the agent's address. Defaults to a hardcoded string in `agent.py`. Change it to get a new address. | +| `CLIENT_SEED` | ❌ Optional | Seed phrase for the client agent. | +| `AGENTVERSE_API_KEY` | ❌ Optional | Only needed to publish on Agentverse. | + +--- + +## 7) Run the Agent + +### Step 1 — Start the weather agent + +```bash +python agent.py +``` + +Expected output: + +``` +INFO: [weather_agent]: Weather Agent started | address: agent1qfx0jmyn... +INFO: [weather_agent]: Waiting for WeatherRequest messages … +``` + +> **Note the agent address** printed on startup — paste it into `client.py` as `WEATHER_AGENT_ADDRESS` if it differs from the default. + +### Step 2 — Run the client (in a second terminal) + +```bash +# Default city (London) +python client.py + +# Or pass any city as an argument +python client.py "Tokyo" +python client.py "São Paulo" +python client.py "Coimbatore" +``` + +--- + +## 8) Expected Output + +``` +──────────────────────────────────────────────── + 🌤 Weather Report — Tokyo, Japan +──────────────────────────────────────────────── + Condition : Partly cloudy + Temperature : 22.4 °C + Feels like : 21.8 °C + Humidity : 63 % + Wind speed : 14.2 km/h +``` + +If the city is not found: + +``` +──────────────────────────────────────────────── + ⚠ Weather Agent Error +──────────────────────────────────────────────── + City 'Atlantis' not found. Please check the spelling. +``` + +--- + +## 9) Demo + +> _Add a screenshot here after running the agent locally._ + +``` +![Weather Agent Demo](./assets/demo1.jpeg) +![Weather Agent Demo](./assets/demo2.jpeg) +``` + +--- + +## 10) Agent Profile + +Not yet published on Agentverse. +Once published, link it here: + +``` +[View Agent Profile](https://agentverse.ai/agents/
) +``` + +--- + +## 11) Architecture + +``` +┌──────────────┐ WeatherRequest(city) ┌──────────────────────┐ +│ client.py │ ────────────────────────► │ agent.py │ +│ (port 8001) │ │ (port 8000) │ +│ │ ◄──────────────────────── │ │ +│ │ WeatherResponse(...) │ 1. Geocoding API │ +└──────────────┘ │ 2. Forecast API │ + └──────────────────────┘ + │ + ▼ + https://open-meteo.com + (free, no auth) +``` + +**Data flow:** + +1. `client.py` sends a `WeatherRequest(city="Tokyo")` to the agent's address. +2. `agent.py` calls the Open-Meteo **Geocoding API** to resolve the city to `(lat, lon)`. +3. `agent.py` calls the Open-Meteo **Forecast API** with `current=` parameters for temperature, humidity, wind speed, and WMO weather code. +4. The agent maps the WMO code to a human-readable description and replies with a `WeatherResponse`. +5. `client.py` receives and pretty-prints the response, then exits. + +--- + +## 12) Troubleshooting + +| Symptom | Fix | +|---|---| +| `ModuleNotFoundError: uagents` | Run `pip install -r requirements.txt` inside your virtual environment. | +| Client prints nothing / times out | Make sure `agent.py` is running in another terminal **before** starting `client.py`. | +| `City not found` for a valid city | Try a more specific name (e.g. `"Mumbai"` instead of `"Bombay"`). | +| `ConnectionError` | Check your internet connection — both `geocoding-api.open-meteo.com` and `api.open-meteo.com` must be reachable. | +| Wrong agent address | Copy the address printed by `agent.py` on startup and paste it as `WEATHER_AGENT_ADDRESS` in `client.py`. | +| Port conflict (`8000` or `8001` already in use) | Change `port=` in `agent.py` / `client.py` and update the `endpoint=` URL to match. | + +--- + +## 13) License + +Apache 2.0 + +--- + diff --git a/contributors/weather_agent/agent.py b/contributors/weather_agent/agent.py new file mode 100644 index 0000000..3fca8c3 --- /dev/null +++ b/contributors/weather_agent/agent.py @@ -0,0 +1,169 @@ +""" +weather-agent/agent.py +----------------------- +A Fetch.ai uAgent that accepts a city name and returns current weather +data from the free Open-Meteo API (no API key required). + +Run: + python agent.py +""" + +import requests +from uagents import Agent, Context + +from models import WeatherRequest, WeatherResponse # ← shared schemas + +# --------------------------------------------------------------------------- +# Geocoding + weather helpers +# --------------------------------------------------------------------------- + +GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search" +WEATHER_URL = "https://api.open-meteo.com/v1/forecast" + +WMO_CODES: dict[int, str] = { + 0: "Clear sky", + 1: "Mainly clear", + 2: "Partly cloudy", + 3: "Overcast", + 45: "Fog", + 48: "Icy fog", + 51: "Light drizzle", + 53: "Moderate drizzle", + 55: "Dense drizzle", + 61: "Slight rain", + 63: "Moderate rain", + 65: "Heavy rain", + 71: "Slight snow", + 73: "Moderate snow", + 75: "Heavy snow", + 77: "Snow grains", + 80: "Slight showers", + 81: "Moderate showers", + 82: "Violent showers", + 85: "Slight snow showers", + 86: "Heavy snow showers", + 95: "Thunderstorm", + 96: "Thunderstorm w/ slight hail", + 99: "Thunderstorm w/ heavy hail", +} + + +def geocode_city(city: str) -> tuple[float, float, str] | None: + try: + resp = requests.get( + GEOCODING_URL, + params={"name": city, "count": 1, "language": "en", "format": "json"}, + timeout=10, + ) + resp.raise_for_status() + results = resp.json().get("results") + if not results: + return None + r = results[0] + parts = [r.get("name", city)] + if r.get("country"): + parts.append(r["country"]) + return r["latitude"], r["longitude"], ", ".join(parts) + except requests.RequestException: + return None + + +def fetch_weather(lat: float, lon: float) -> dict | None: + try: + resp = requests.get( + WEATHER_URL, + params={ + "latitude": lat, + "longitude": lon, + "current": [ + "temperature_2m", + "apparent_temperature", + "relative_humidity_2m", + "wind_speed_10m", + "weathercode", + ], + "timezone": "auto", + }, + timeout=10, + ) + resp.raise_for_status() + return resp.json().get("current", {}) + except requests.RequestException: + return None + + +# --------------------------------------------------------------------------- +# Agent +# --------------------------------------------------------------------------- + +AGENT_SEED = "weather-agent-open-meteo-seed-v1" + +weather_agent = Agent( + name="weather_agent", + seed=AGENT_SEED, + port=8000, + endpoint=["http://localhost:8000/submit"], +) + + +@weather_agent.on_event("startup") +async def on_start(ctx: Context): + ctx.logger.info(f"Weather Agent started | address: {ctx.agent.address}") + ctx.logger.info("Waiting for WeatherRequest messages …") + + +@weather_agent.on_message(model=WeatherRequest, replies={WeatherResponse}) +async def handle_weather_request(ctx: Context, sender: str, msg: WeatherRequest): + city = msg.city.strip() + ctx.logger.info(f"Request for '{city}' from {sender}") + + geo = geocode_city(city) + if geo is None: + await ctx.send( + sender, + WeatherResponse( + city=city, + temperature_c=0, + feels_like_c=0, + wind_speed_kmh=0, + humidity_percent=0, + description="", + error=f"City '{city}' not found. Please check the spelling.", + ), + ) + return + + lat, lon, display_name = geo + current = fetch_weather(lat, lon) + if current is None: + await ctx.send( + sender, + WeatherResponse( + city=display_name, + temperature_c=0, + feels_like_c=0, + wind_speed_kmh=0, + humidity_percent=0, + description="", + error="Failed to retrieve weather data. Please try again.", + ), + ) + return + + wmo_code = int(current.get("weathercode", 0)) + response = WeatherResponse( + city=display_name, + temperature_c=round(current.get("temperature_2m", 0), 1), + feels_like_c=round(current.get("apparent_temperature", 0), 1), + wind_speed_kmh=round(current.get("wind_speed_10m", 0), 1), + humidity_percent=int(current.get("relative_humidity_2m", 0)), + description=WMO_CODES.get(wmo_code, f"WMO code {wmo_code}"), + ) + ctx.logger.info( + f"Replying: {display_name} | {response.temperature_c}°C | {response.description}" + ) + await ctx.send(sender, response) + + +if __name__ == "__main__": + weather_agent.run() diff --git a/contributors/weather_agent/assets/demo1.jpeg b/contributors/weather_agent/assets/demo1.jpeg new file mode 100644 index 0000000..e36d40b Binary files /dev/null and b/contributors/weather_agent/assets/demo1.jpeg differ diff --git a/contributors/weather_agent/assets/demo2.jpeg b/contributors/weather_agent/assets/demo2.jpeg new file mode 100644 index 0000000..d3bf9bc Binary files /dev/null and b/contributors/weather_agent/assets/demo2.jpeg differ diff --git a/contributors/weather_agent/client.py b/contributors/weather_agent/client.py new file mode 100644 index 0000000..fe3d197 --- /dev/null +++ b/contributors/weather_agent/client.py @@ -0,0 +1,68 @@ +""" +weather-agent/client.py +------------------------ +A uAgents client that sends a WeatherRequest to the weather agent +and pretty-prints the WeatherResponse it receives back. + +Usage: + python client.py # defaults to London + python client.py "Tokyo" + python client.py "Chennai" +""" + +import sys +from uagents import Agent, Context + +from models import WeatherRequest, WeatherResponse # ← shared schemas + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +# Copy the address printed by agent.py on startup and paste it here. +WEATHER_AGENT_ADDRESS = ( + "agent1q200mh5dht5rpnz3yk0zu8fjndd2m9pdyup7e9rprnjvnxusaqjzg79ttmd" +) + +CITY = sys.argv[1] if len(sys.argv) > 1 else "London" + +# --------------------------------------------------------------------------- +# Client agent +# --------------------------------------------------------------------------- + +client = Agent( + name="weather_client", + seed="weather-client-seed-v1", + port=8001, + endpoint=["http://localhost:8001/submit"], +) + + +def _banner(title: str) -> str: + return f"\n{'─' * 50}\n {title}\n{'─' * 50}" + + +@client.on_event("startup") +async def send_request(ctx: Context): + ctx.logger.info(f"Client started | address: {ctx.agent.address}") + ctx.logger.info(f"Querying weather for '{CITY}' …") + await ctx.send(WEATHER_AGENT_ADDRESS, WeatherRequest(city=CITY)) + + +@client.on_message(model=WeatherResponse) +async def handle_response(ctx: Context, sender: str, msg: WeatherResponse): + if msg.error: + print(_banner("⚠ Weather Agent Error")) + print(f" {msg.error}") + else: + print(_banner(f"🌤 Weather Report — {msg.city}")) + print(f" Condition : {msg.description}") + print(f" Temperature : {msg.temperature_c} °C") + print(f" Feels like : {msg.feels_like_c} °C") + print(f" Humidity : {msg.humidity_percent} %") + print(f" Wind speed : {msg.wind_speed_kmh} km/h") + print() + + +if __name__ == "__main__": + client.run() diff --git a/contributors/weather_agent/models.py b/contributors/weather_agent/models.py new file mode 100644 index 0000000..b5606a5 --- /dev/null +++ b/contributors/weather_agent/models.py @@ -0,0 +1,29 @@ +""" +weather-agent/models.py +------------------------ +Single source of truth for all message schemas shared between +agent.py and client.py. + +uAgents derives a schema digest from the class definition — both +sides MUST import from the same module to get identical digests. +""" + +from uagents import Model + + +class WeatherRequest(Model): + """Sent by the client: the city name to look up.""" + + city: str + + +class WeatherResponse(Model): + """Returned by the agent: current weather conditions.""" + + city: str + temperature_c: float + feels_like_c: float + wind_speed_kmh: float + humidity_percent: int + description: str + error: str = ""