Split into Weather sub-agent + Daily Briefing orchestrator
This commit is contained in:
@@ -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()
|
||||||
@@ -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"]
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user