From e07341faf9fe3dd96001a1e87ebc757ea9e43c3e Mon Sep 17 00:00:00 2001 From: Eric Jungbauer Date: Mon, 13 Apr 2026 01:31:53 +0000 Subject: [PATCH] Split into Weather sub-agent + Daily Briefing orchestrator --- agents/daily_briefing.py | 139 +++++++++++++++++++++++++++++++++++++++ agents/shared.py | 90 +++++++++++++++++++++++++ agents/weather_agent.py | 112 +++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 agents/daily_briefing.py create mode 100644 agents/shared.py create mode 100644 agents/weather_agent.py diff --git a/agents/daily_briefing.py b/agents/daily_briefing.py new file mode 100644 index 0000000..bdfdfd0 --- /dev/null +++ b/agents/daily_briefing.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +Daily Briefing Agent (Orchestrator) +Calls sub-agents (weather, etc.), collates their output into a single +markdown briefing, and posts it to the Outline wiki. + +Hierarchy: Eric's Daily Briefing → {Year} → {Month} → Daily Briefing — {date} +""" + +import sys +from datetime import datetime +from shared import ( + MT, WIKI_API, WIKI_COLLECTION_ID, WIKI_PARENT_DOC_ID, MONTH_NAMES, + api_request, log_run, wiki_headers, find_child_doc, ensure_child_doc, +) + +AGENT_ID = "daily-briefing" + + +def collect_sections(): + """Run each sub-agent and collect markdown sections. + + Returns a list of (section_name, markdown, summary) tuples. + Failed sub-agents are logged but don't stop the briefing. + """ + sections = [] + + # --- Weather --- + try: + from weather_agent import run as weather_run + md, summary = weather_run() + sections.append(("Weather", md, summary)) + print(f" Weather: {summary}") + except Exception as e: + print(f" Weather failed: {e}", file=sys.stderr) + sections.append(("Weather", "## Weather\n\n*Weather data unavailable.*\n", f"error: {e}")) + + # --- Future sub-agents go here --- + # try: + # from calendar_agent import run as calendar_run + # md, summary = calendar_run() + # sections.append(("Calendar", md, summary)) + # except Exception as e: + # sections.append(("Calendar", "## Calendar\n\n*Calendar data unavailable.*\n", f"error: {e}")) + + return sections + + +def compose_briefing(sections): + """Compose the full daily briefing markdown from sub-agent sections.""" + now = datetime.now(MT) + date_str = now.strftime("%A, %B %d, %Y") + + md = f"# Daily Briefing\n" + md += f"**{date_str}** | Providence, Utah\n\n" + md += "---\n\n" + + for _name, section_md, _summary in sections: + md += section_md + "\n\n" + + md += "---\n" + md += f"*Generated at {now.strftime('%I:%M %p MT')} by Daily Briefing Agent*\n" + + return md + + +def post_to_wiki(markdown, date_str): + """Post the briefing to wiki under Year/Month hierarchy.""" + now = datetime.now(MT) + year_str = str(now.year) + month_str = MONTH_NAMES[now.month] + + year_id = ensure_child_doc( + WIKI_PARENT_DOC_ID, year_str, + f"Daily briefings for {year_str}.", + ) + month_id = ensure_child_doc( + year_id, month_str, + f"Daily briefings for {month_str} {year_str}.", + ) + + title = f"Daily Briefing — {date_str}" + doc_id = find_child_doc(month_id, title) + + if doc_id: + api_request( + f"{WIKI_API}/documents.update", + data={"id": doc_id, "text": markdown, "publish": True}, + headers=wiki_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=wiki_headers(), + method="POST", + ) + return result["data"]["id"], "created" + + +def main(): + try: + print("Collecting sub-agent data...") + sections = collect_sections() + + print("Composing briefing...") + markdown = compose_briefing(sections) + date_str = datetime.now(MT).strftime("%Y-%m-%d") + + print("Posting to wiki...") + doc_id, action = post_to_wiki(markdown, date_str) + print(f"Wiki doc {action}: {doc_id}") + + summaries = "; ".join(f"{name}: {s}" for name, _, s in sections) + output = f"Briefing {action}. {summaries}" + log_run(AGENT_ID, "success", output=output, metadata={ + "wiki_doc_id": doc_id, + "action": action, + "sub_agents": [name for name, _, _ in sections], + }) + 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(AGENT_ID, "failed", err=err_msg) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/agents/shared.py b/agents/shared.py new file mode 100644 index 0000000..801542d --- /dev/null +++ b/agents/shared.py @@ -0,0 +1,90 @@ +"""Shared utilities for all agents.""" + +import json +import os +import sys +from datetime import datetime +from zoneinfo import ZoneInfo +from urllib import request + +MT = ZoneInfo("America/Denver") + +DASHBOARD_API = os.environ.get("DASHBOARD_API", "http://localhost:8550") + +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 + +MONTH_NAMES = [ + "", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", +] + + +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 log_run(agent_id, status, output="", err="", metadata=None): + """Log a 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 wiki_headers(): + return {"Authorization": f"Bearer {WIKI_TOKEN}"} + + +def find_child_doc(parent_id, title): + """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=wiki_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): + """Find or create a child doc under a parent. Returns doc ID.""" + doc_id = find_child_doc(parent_id, title) + 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=wiki_headers(), + method="POST", + ) + print(f"Created wiki doc: {title}") + return result["data"]["id"] diff --git a/agents/weather_agent.py b/agents/weather_agent.py new file mode 100644 index 0000000..5056a74 --- /dev/null +++ b/agents/weather_agent.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +Weather Agent +Fetches weather for Providence, UT and returns structured data + markdown section. +Called by the Daily Briefing agent. Can also run standalone. +""" + +import sys +from datetime import datetime +from shared import MT, api_request, log_run + +AGENT_ID = "weather" + +# Providence, UT +LAT = 41.7064 +LON = -111.8133 + +WEATHER_URL = ( + f"https://api.open-meteo.com/v1/forecast?" + f"latitude={LAT}&longitude={LON}" + f"¤t=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 fetch_weather(): + """Fetch weather data from Open-Meteo.""" + return api_request(WEATHER_URL) + + +def format_section(weather): + """Format weather into a markdown section and a one-line summary.""" + now = datetime.now(MT) + 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 = "## Weather\n\n" + md += "### Current Conditions\n\n" + md += "| | |\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 += "### 7-Day Forecast\n\n" + md += "| Day | Condition | High | Low | Precip | Wind |\n" + md += "|-----|-----------|------|-----|--------|------|\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 = 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" + + summary = f"{condition}, {temp}°F (feels like {feels}°F), wind {wind} mph" + return md, summary + + +def run(): + """Run the weather agent. Returns (markdown_section, summary) or raises.""" + weather = fetch_weather() + section, summary = format_section(weather) + log_run(AGENT_ID, "success", output=summary) + return section, summary + + +if __name__ == "__main__": + try: + section, summary = run() + print(section) + print(f"\nSummary: {summary}") + except Exception as e: + err_msg = f"{type(e).__name__}: {e}" + print(f"Error: {err_msg}", file=sys.stderr) + log_run(AGENT_ID, "failed", err=err_msg) + sys.exit(1)