Files
ai-agents/agents/weather_briefing.py

262 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""
Weather Briefing Agent
Fetches weather for Providence, UT and posts to Outline wiki.
Logs run to the Agent Dashboard API.
"""
import json
import os
import sys
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
from urllib import request, error, parse
MT = ZoneInfo("America/Denver")
# --- Config ---
DASHBOARD_API = os.environ.get("DASHBOARD_API", "http://localhost:8550")
AGENT_ID = "weather-briefing"
WIKI_API = "https://wiki.jfamily.io/api"
WIKI_TOKEN = os.environ.get("WIKI_TOKEN", "ol_api_yHXypRyqf4CscWDzPluGfPev9GhdFg6mwrXwkT")
WIKI_COLLECTION_ID = os.environ.get("WIKI_COLLECTION_ID", "9d9e471c-84cd-4ba7-bae5-c70f61805228")
WIKI_PARENT_DOC_ID = os.environ.get("WIKI_PARENT_DOC_ID", "2a891fe8-579b-450b-a663-de93915896b7") # Eric's Daily Briefing
# Providence, UT
LAT = 41.7064
LON = -111.8133
WEATHER_URL = (
f"https://api.open-meteo.com/v1/forecast?"
f"latitude={LAT}&longitude={LON}"
f"&current=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m"
f"&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,sunrise,sunset"
f"&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch"
f"&timezone=America/Denver&forecast_days=7"
)
WMO_CODES = {
0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
45: "Foggy", 48: "Icy fog", 51: "Light drizzle", 53: "Drizzle",
55: "Heavy drizzle", 61: "Light rain", 63: "Rain", 65: "Heavy rain",
66: "Freezing rain", 67: "Heavy freezing rain",
71: "Light snow", 73: "Snow", 75: "Heavy snow", 77: "Snow grains",
80: "Light showers", 81: "Showers", 82: "Heavy showers",
85: "Light snow showers", 86: "Heavy snow showers",
95: "Thunderstorm", 96: "Thunderstorm w/ hail", 99: "Severe thunderstorm",
}
DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def api_request(url, data=None, headers=None, method="GET"):
"""Simple HTTP helper using urllib."""
if data is not None:
data = json.dumps(data).encode("utf-8")
req = request.Request(url, data=data, headers=headers or {}, method=method)
if data:
req.add_header("Content-Type", "application/json")
with request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
def fetch_weather():
"""Fetch weather data from Open-Meteo."""
return api_request(WEATHER_URL)
def format_briefing(weather):
"""Format weather data into markdown."""
now = datetime.now(MT)
date_str = now.strftime("%A, %B %d, %Y")
current = weather["current"]
daily = weather["daily"]
condition = WMO_CODES.get(current["weather_code"], "Unknown")
temp = round(current["temperature_2m"])
feels = round(current["apparent_temperature"])
wind = round(current["wind_speed_10m"])
humidity = current["relative_humidity_2m"]
md = f"# Daily Weather Briefing\n"
md += f"**{date_str}** | Providence, Utah\n\n"
md += f"---\n\n"
md += f"## Current Conditions\n\n"
md += f"| | |\n|---|---|\n"
md += f"| **Condition** | {condition} |\n"
md += f"| **Temperature** | {temp}°F (feels like {feels}°F) |\n"
md += f"| **Wind** | {wind} mph |\n"
md += f"| **Humidity** | {humidity}% |\n\n"
md += f"## 7-Day Forecast\n\n"
md += f"| Day | Condition | High | Low | Precip | Wind |\n"
md += f"|-----|-----------|------|-----|--------|------|\n"
for i in range(len(daily["time"])):
d = datetime.strptime(daily["time"][i], "%Y-%m-%d")
day_name = DAY_NAMES[d.weekday()]
if i == 0:
day_name = "**Today**"
elif i == 1:
day_name = "Tomorrow"
cond = WMO_CODES.get(daily["weather_code"][i], "?")
hi = round(daily["temperature_2m_max"][i])
lo = round(daily["temperature_2m_min"][i])
precip = daily["precipitation_sum"][i]
wind_max = round(daily["wind_speed_10m_max"][i])
precip_str = f'{precip}"' if precip > 0 else "-"
md += f"| {day_name} | {cond} | {hi}°F | {lo}°F | {precip_str} | {wind_max} mph |\n"
# Sunrise/sunset for today
sunrise = daily["sunrise"][0].split("T")[1] if daily["sunrise"][0] else "?"
sunset = daily["sunset"][0].split("T")[1] if daily["sunset"][0] else "?"
md += f"\n**Sunrise:** {sunrise} | **Sunset:** {sunset}\n"
md += f"\n---\n*Generated at {now.strftime('%I:%M %p MT')} by Weather Briefing Agent*\n"
return md, f"{condition}, {temp}°F (feels like {feels}°F), wind {wind} mph"
MONTH_NAMES = [
"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
]
def find_child_doc(parent_id, title, headers):
"""Search for a child doc by title under a given parent."""
result = api_request(
f"{WIKI_API}/documents.search",
data={"query": title, "collectionId": WIKI_COLLECTION_ID},
headers=headers,
method="POST",
)
for doc in result.get("data", []):
d = doc.get("document", {})
if d.get("title") == title and d.get("parentDocumentId") == parent_id:
return d["id"]
return None
def ensure_child_doc(parent_id, title, body, headers):
"""Find or create a child doc under a parent. Returns doc ID."""
doc_id = find_child_doc(parent_id, title, headers)
if doc_id:
return doc_id
result = api_request(
f"{WIKI_API}/documents.create",
data={
"title": title,
"text": body,
"collectionId": WIKI_COLLECTION_ID,
"parentDocumentId": parent_id,
"publish": True,
},
headers=headers,
method="POST",
)
print(f"Created wiki doc: {title}")
return result["data"]["id"]
def post_to_wiki(markdown, date_str):
"""Create or update a daily briefing doc in Outline.
Hierarchy: Eric's Daily Briefing → {Year}{Month} → Daily Briefing — {date}
"""
headers = {
"Authorization": f"Bearer {WIKI_TOKEN}",
}
now = datetime.now(MT)
year_str = str(now.year)
month_str = MONTH_NAMES[now.month]
# Ensure year doc exists under Eric's Daily Briefing
year_id = ensure_child_doc(
WIKI_PARENT_DOC_ID, year_str,
f"Daily briefings for {year_str}.", headers,
)
# Ensure month doc exists under year
month_id = ensure_child_doc(
year_id, month_str,
f"Daily briefings for {month_str} {year_str}.", headers,
)
# Search for existing doc with today's date under the month
title = f"Daily Briefing — {date_str}"
doc_id = find_child_doc(month_id, title, headers)
if doc_id:
api_request(
f"{WIKI_API}/documents.update",
data={"id": doc_id, "text": markdown, "publish": True},
headers=headers,
method="POST",
)
return doc_id, "updated"
else:
result = api_request(
f"{WIKI_API}/documents.create",
data={
"title": title,
"text": markdown,
"collectionId": WIKI_COLLECTION_ID,
"parentDocumentId": month_id,
"publish": True,
},
headers=headers,
method="POST",
)
return result["data"]["id"], "created"
def log_run(status, output="", err="", metadata=None):
"""Log this run to the dashboard API."""
try:
api_request(
f"{DASHBOARD_API}/api/agents/{AGENT_ID}/runs",
data={
"status": status,
"output": output,
"error": err,
"metadata": metadata or {},
},
method="POST",
)
except Exception as e:
print(f"Warning: failed to log run to dashboard: {e}", file=sys.stderr)
def main():
try:
print("Fetching weather...")
weather = fetch_weather()
print("Formatting briefing...")
date_str = datetime.now(MT).strftime("%Y-%m-%d")
markdown, summary = format_briefing(weather)
print("Posting to wiki...")
doc_id, action = post_to_wiki(markdown, date_str)
print(f"Wiki doc {action}: {doc_id}")
output = f"Weather: {summary}. Wiki doc {action}."
log_run("success", output=output, metadata={"wiki_doc_id": doc_id, "action": action})
print(f"Done: {output}")
except Exception as e:
err_msg = f"{type(e).__name__}: {e}"
print(f"Error: {err_msg}", file=sys.stderr)
log_run("failed", err=err_msg)
sys.exit(1)
if __name__ == "__main__":
main()