API Clients + structured JSON results: app-level tokens for Synap/WSIT integration

- 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
This commit is contained in:
Eric Jungbauer
2026-04-20 17:54:32 +00:00
parent f01553c511
commit 043aa18f3f
8 changed files with 983 additions and 111 deletions
+116 -44
View File
@@ -14,10 +14,29 @@ from shared import (
)
def collect_sections(config):
"""Run each sub-agent and collect markdown sections.
def _invoke_sub_agent(module_name, **kwargs):
"""Call a sub-agent, preferring its run_structured() contract if available.
Returns a list of (section_name, markdown, summary) tuples.
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 = []
@@ -25,88 +44,114 @@ def collect_sections(config):
# --- Weather ---
try:
from weather_agent import run as weather_run
md, summary = weather_run(location=location)
sections.append(("Weather", md, summary))
print(f" Weather: {summary}")
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(("Weather", "## Weather\n\n*Weather data unavailable.*\n", f"error: {e}"))
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:
from calendar_agent import run as calendar_run
md, summary = calendar_run(calendars_config=calendars)
sections.append(("Calendar", md, summary))
print(f" Calendar: {summary}")
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(("Calendar", "## Calendar\n\n*Calendar data unavailable.*\n", f"error: {e}"))
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:
from reminders_agent import run as reminders_run
md, summary = reminders_run(reminders_config=reminder_sources, mode=reminder_mode)
sections.append(("Reminders", md, summary))
print(f" Reminders: {summary}")
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(("Reminders", "## Reminders\n\n*Unavailable.*\n", f"error: {e}"))
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:
from notes_agent import run as notes_run
md, summary = notes_run(config=notes_config)
sections.append(("Notes", md, summary))
print(f" Notes: {summary}")
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(("Notes", "## Recent Notes\n\n*Unavailable.*\n", f"error: {e}"))
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:
from reading_list_agent import run as reading_list_run
md, summary = reading_list_run(config=reading_config)
sections.append(("Reading List", md, summary))
print(f" Reading List: {summary}")
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(("Reading List", "## Reading List\n\n*Unavailable.*\n", f"error: {e}"))
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:
# Fetch this user's project-monitor instances that are set to include in briefing
pm_instances = api_request(
f"{DASHBOARD_API}/api/instances/by-user/{user_id}?catalog_id=project-monitor",
retries=1,
)
project_sections = []
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
md, summary = pm_run(pm_config, user_id=user_id, instance_id=pm.get("id"))
project_sections.append(md)
print(f" Project [{pm_config.get('project_name', '?')}]: {summary[:80]}")
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:
combined = "## Projects\n\n" + "\n\n".join(project_sections)
sections.append(("Projects", combined, f"{len(project_sections)} project(s)"))
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)
@@ -127,8 +172,8 @@ def compose_briefing(config, sections):
md += f"**{date_str}** | {loc_label}\n\n"
md += "---\n\n"
for _name, section_md, _summary in sections:
md += section_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"
@@ -136,6 +181,29 @@ def compose_briefing(config, sections):
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)
@@ -218,13 +286,17 @@ def run(config):
doc_id, action = post_to_wiki(config, markdown, date_str)
print(f"Wiki doc {action}: {doc_id}")
summaries = "; ".join(f"{name}: {s}" for name, _, s in sections)
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=output, instance_id=instance_id, metadata={
"wiki_doc_id": doc_id,
"action": action,
"sub_agents": [name for name, _, _ in sections],
})
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: