043aa18f3f
- New api_clients + api_client_scopes tables; tokens scoped per-instance
- Admin UI tab at /admin for token create/rotate/revoke/delete with one-time reveal
- Dual-auth dependency (user session OR Bearer app token) on trigger + runs endpoints
- /api/instances/{id}/trigger pre-creates a run and returns run_id + cached last_result instantly
- New GET /api/runs/{id} for polling
- Generic trigger path for sub-agent instances (weather, calendar, etc.)
- runs.result column for structured JSON alongside markdown output
- agent_catalog.result_schema describes each agent's result shape
- Weather, daily-briefing, project-monitor retrofitted to emit structured results
- log_run: env INSTANCE_ID/RUN_ID only used when target matches, so nested sub-agents don't clobber parent runs
- Wiki docs: API Clients & Token Scoping + Calling Agents From Your Apps
307 lines
12 KiB
Python
307 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Daily Briefing Engine (Reusable)
|
|
Collects sub-agent data, composes a markdown briefing, and posts to wiki.
|
|
Called by person-specific wrappers (eric_briefing.py, angela_briefing.py, etc.)
|
|
with a config dict specifying location, wiki parent, and agent ID.
|
|
"""
|
|
|
|
import sys
|
|
from datetime import datetime
|
|
from shared import (
|
|
MT, DASHBOARD_API, WIKI_API, WIKI_COLLECTION_ID as DEFAULT_WIKI_COLLECTION, MONTH_NAMES,
|
|
api_request, log_run, wiki_headers, find_child_doc, ensure_child_doc,
|
|
)
|
|
|
|
|
|
def _invoke_sub_agent(module_name, **kwargs):
|
|
"""Call a sub-agent, preferring its run_structured() contract if available.
|
|
|
|
Returns dict with keys: markdown, summary, result (result may be None for
|
|
agents that haven't been retrofitted yet). Raises on agent failure.
|
|
"""
|
|
mod = __import__(module_name)
|
|
if hasattr(mod, "run_structured"):
|
|
out = mod.run_structured(**kwargs)
|
|
return {
|
|
"markdown": out.get("markdown", ""),
|
|
"summary": out.get("summary", ""),
|
|
"result": out.get("result"),
|
|
}
|
|
# Legacy contract: returns (markdown, summary)
|
|
md, summary = mod.run(**kwargs)
|
|
return {"markdown": md, "summary": summary, "result": None}
|
|
|
|
|
|
def collect_sections(config):
|
|
"""Run each sub-agent and collect output.
|
|
|
|
Returns a list of section dicts: {name, key, markdown, summary, result}.
|
|
Failed sub-agents are logged but don't stop the briefing.
|
|
"""
|
|
sections = []
|
|
location = config.get("location")
|
|
|
|
# --- Weather ---
|
|
try:
|
|
out = _invoke_sub_agent("weather_agent", location=location)
|
|
sections.append({"name": "Weather", "key": "weather", **out})
|
|
print(f" Weather: {out['summary']}")
|
|
except Exception as e:
|
|
print(f" Weather failed: {e}", file=sys.stderr)
|
|
sections.append({"name": "Weather", "key": "weather",
|
|
"markdown": "## Weather\n\n*Weather data unavailable.*\n",
|
|
"summary": f"error: {e}", "result": None, "error": str(e)})
|
|
|
|
# --- Calendar ---
|
|
calendars = config.get("calendars", [])
|
|
try:
|
|
out = _invoke_sub_agent("calendar_agent", calendars_config=calendars)
|
|
sections.append({"name": "Calendar", "key": "calendar", **out})
|
|
print(f" Calendar: {out['summary']}")
|
|
except Exception as e:
|
|
print(f" Calendar failed: {e}", file=sys.stderr)
|
|
sections.append({"name": "Calendar", "key": "calendar",
|
|
"markdown": "## Calendar\n\n*Calendar data unavailable.*\n",
|
|
"summary": f"error: {e}", "result": None, "error": str(e)})
|
|
|
|
# --- Reminders (CalDAV VTODO) ---
|
|
reminder_sources = config.get("reminder_sources", [])
|
|
reminder_mode = config.get("reminder_mode", "due_today_3days")
|
|
if reminder_sources:
|
|
try:
|
|
out = _invoke_sub_agent("reminders_agent", reminders_config=reminder_sources, mode=reminder_mode)
|
|
sections.append({"name": "Reminders", "key": "reminders", **out})
|
|
print(f" Reminders: {out['summary']}")
|
|
except Exception as e:
|
|
print(f" Reminders failed: {e}", file=sys.stderr)
|
|
sections.append({"name": "Reminders", "key": "reminders",
|
|
"markdown": "## Reminders\n\n*Unavailable.*\n",
|
|
"summary": f"error: {e}", "result": None, "error": str(e)})
|
|
|
|
# --- Notes (via Mac bridge) ---
|
|
notes_config = config.get("notes", {})
|
|
if notes_config.get("enabled", False):
|
|
try:
|
|
out = _invoke_sub_agent("notes_agent", config=notes_config)
|
|
sections.append({"name": "Notes", "key": "notes", **out})
|
|
print(f" Notes: {out['summary']}")
|
|
except Exception as e:
|
|
print(f" Notes failed: {e}", file=sys.stderr)
|
|
sections.append({"name": "Notes", "key": "notes",
|
|
"markdown": "## Recent Notes\n\n*Unavailable.*\n",
|
|
"summary": f"error: {e}", "result": None, "error": str(e)})
|
|
|
|
# --- Reading List (via Mac bridge) ---
|
|
reading_config = config.get("reading_list", {})
|
|
if reading_config.get("enabled", False):
|
|
try:
|
|
out = _invoke_sub_agent("reading_list_agent", config=reading_config)
|
|
sections.append({"name": "Reading List", "key": "reading_list", **out})
|
|
print(f" Reading List: {out['summary']}")
|
|
except Exception as e:
|
|
print(f" Reading List failed: {e}", file=sys.stderr)
|
|
sections.append({"name": "Reading List", "key": "reading_list",
|
|
"markdown": "## Reading List\n\n*Unavailable.*\n",
|
|
"summary": f"error: {e}", "result": None, "error": str(e)})
|
|
|
|
# --- Project Monitors (LLM-powered) ---
|
|
instance_id = config.get("instance_id", 0)
|
|
user_id = config.get("user_id", 0)
|
|
if user_id:
|
|
try:
|
|
pm_instances = api_request(
|
|
f"{DASHBOARD_API}/api/instances/by-user/{user_id}?catalog_id=project-monitor",
|
|
retries=1,
|
|
)
|
|
project_sections_md = []
|
|
project_results = []
|
|
for pm in pm_instances:
|
|
pm_config = pm.get("config", {})
|
|
if str(pm_config.get("include_in_briefing", "false")).lower() != "true":
|
|
continue
|
|
try:
|
|
from project_monitor import run as pm_run
|
|
pm_out = pm_run(pm_config, user_id=user_id, instance_id=pm.get("id"))
|
|
# project_monitor.run may return (md, summary) or {markdown, summary, result}
|
|
if isinstance(pm_out, dict):
|
|
project_sections_md.append(pm_out.get("markdown", ""))
|
|
project_results.append({
|
|
"project_name": pm_config.get("project_name", ""),
|
|
"summary": pm_out.get("summary", ""),
|
|
"result": pm_out.get("result"),
|
|
})
|
|
print(f" Project [{pm_config.get('project_name', '?')}]: {pm_out.get('summary','')[:80]}")
|
|
else:
|
|
md, summary = pm_out
|
|
project_sections_md.append(md)
|
|
project_results.append({
|
|
"project_name": pm_config.get("project_name", ""),
|
|
"summary": summary,
|
|
"result": None,
|
|
})
|
|
print(f" Project [{pm_config.get('project_name', '?')}]: {summary[:80]}")
|
|
except Exception as e:
|
|
print(f" Project [{pm_config.get('project_name', '?')}] failed: {e}", file=sys.stderr)
|
|
|
|
if project_sections_md:
|
|
combined = "## Projects\n\n" + "\n\n".join(project_sections_md)
|
|
sections.append({
|
|
"name": "Projects", "key": "projects",
|
|
"markdown": combined,
|
|
"summary": f"{len(project_sections_md)} project(s)",
|
|
"result": {"projects": project_results},
|
|
})
|
|
except Exception as e:
|
|
print(f" Project monitors skipped: {e}", file=sys.stderr)
|
|
|
|
return sections
|
|
|
|
|
|
def compose_briefing(config, sections):
|
|
"""Compose the full daily briefing markdown from sub-agent sections."""
|
|
now = datetime.now(MT)
|
|
date_str = now.strftime("%A, %B %d, %Y")
|
|
person = config.get("person", "")
|
|
location = config.get("location", {})
|
|
loc_label = location.get("name", "")
|
|
if location.get("state"):
|
|
loc_label += f", {location['state']}"
|
|
|
|
md = f"# {person}'s Daily Briefing\n"
|
|
md += f"**{date_str}** | {loc_label}\n\n"
|
|
md += "---\n\n"
|
|
|
|
for s in sections:
|
|
md += s["markdown"] + "\n\n"
|
|
|
|
md += "---\n"
|
|
md += f"*Generated at {now.strftime('%I:%M %p MT')} by {person}'s Daily Briefing Agent*\n"
|
|
|
|
return md
|
|
|
|
|
|
def compose_result(config, sections, wiki_doc_id=None, wiki_action=None):
|
|
"""Compose the structured result dict that API consumers read. Mirrors the markdown but
|
|
as data — each sub-agent's result is nested by key."""
|
|
now = datetime.now(MT)
|
|
return {
|
|
"date": now.strftime("%Y-%m-%d"),
|
|
"generated_at": now.isoformat(),
|
|
"person": config.get("person", ""),
|
|
"location": config.get("location", {}),
|
|
"sections": {
|
|
s["key"]: {
|
|
"name": s["name"],
|
|
"summary": s.get("summary", ""),
|
|
"result": s.get("result"), # may be None for not-yet-retrofitted agents
|
|
"error": s.get("error"),
|
|
}
|
|
for s in sections
|
|
},
|
|
"wiki_doc_id": wiki_doc_id,
|
|
"wiki_action": wiki_action,
|
|
}
|
|
|
|
|
|
def post_to_wiki(config, markdown, date_str):
|
|
"""Post the briefing to wiki under Year/Month hierarchy."""
|
|
wiki_collection = config.get("wiki_collection_id", DEFAULT_WIKI_COLLECTION)
|
|
wiki_parent_id = config["wiki_parent_doc_id"]
|
|
now = datetime.now(MT)
|
|
year_str = str(now.year)
|
|
month_str = MONTH_NAMES[now.month]
|
|
|
|
year_id = ensure_child_doc(
|
|
wiki_parent_id, year_str,
|
|
f"Daily briefings for {year_str}.",
|
|
collection_id=wiki_collection,
|
|
)
|
|
month_id = ensure_child_doc(
|
|
year_id, month_str,
|
|
f"Daily briefings for {month_str} {year_str}.",
|
|
collection_id=wiki_collection,
|
|
)
|
|
|
|
title = f"Daily Briefing — {date_str}"
|
|
doc_id = find_child_doc(month_id, title, collection_id=wiki_collection)
|
|
|
|
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,
|
|
"parentDocumentId": month_id,
|
|
"publish": True,
|
|
},
|
|
headers=wiki_headers(),
|
|
method="POST",
|
|
)
|
|
return result["data"]["id"], "created"
|
|
|
|
|
|
def run(config):
|
|
"""Run the full daily briefing pipeline for a given config.
|
|
|
|
Config keys:
|
|
person (str): Person's name (e.g., "Eric")
|
|
agent_id (str): Dashboard agent ID (e.g., "eric-daily-briefing")
|
|
wiki_parent_doc_id (str): Outline doc ID for this person's briefing root
|
|
location (dict): {name, state, country, lat, lon}
|
|
"""
|
|
agent_id = config["agent_id"]
|
|
instance_id = config.get("instance_id", 0)
|
|
|
|
# Fetch live config from dashboard API, merge over defaults
|
|
if instance_id:
|
|
try:
|
|
live_config = api_request(f"{DASHBOARD_API}/api/instances/{instance_id}/config")
|
|
if live_config.get("location"):
|
|
config["location"] = live_config["location"]
|
|
print(f"Using live config location: {config['location'].get('name', '?')}")
|
|
if live_config.get("calendars"):
|
|
config["calendars"] = live_config["calendars"]
|
|
except Exception as e:
|
|
print(f"Could not fetch live config, using defaults: {e}")
|
|
|
|
try:
|
|
print(f"Collecting sub-agent data for {config['person']}...")
|
|
sections = collect_sections(config)
|
|
|
|
print("Composing briefing...")
|
|
markdown = compose_briefing(config, sections)
|
|
date_str = datetime.now(MT).strftime("%Y-%m-%d")
|
|
|
|
print("Posting to wiki...")
|
|
doc_id, action = post_to_wiki(config, markdown, date_str)
|
|
print(f"Wiki doc {action}: {doc_id}")
|
|
|
|
result = compose_result(config, sections, wiki_doc_id=doc_id, wiki_action=action)
|
|
summaries = "; ".join(f"{s['name']}: {s.get('summary','')}" for s in sections)
|
|
output = f"Briefing {action}. {summaries}"
|
|
log_run(agent_id, "success", output=markdown, instance_id=instance_id,
|
|
result=result,
|
|
metadata={
|
|
"wiki_doc_id": doc_id,
|
|
"action": action,
|
|
"sub_agents": [s["name"] for s in sections],
|
|
"summary": summaries,
|
|
})
|
|
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, instance_id=instance_id)
|
|
sys.exit(1)
|