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:
+101
-8
@@ -5,9 +5,10 @@ Fetches weather for a configurable location and returns structured data + markdo
|
||||
Called by Daily Briefing agents with location config. Can also run standalone.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from shared import MT, api_request, log_run
|
||||
from shared import MT, api_request, log_run, get_instance_config
|
||||
|
||||
AGENT_ID = "weather"
|
||||
|
||||
@@ -116,18 +117,106 @@ def format_section(weather, location=None):
|
||||
return md, summary
|
||||
|
||||
|
||||
def run(location=None):
|
||||
"""Run the weather agent. Returns (markdown_section, summary) or raises."""
|
||||
def build_result(weather, location):
|
||||
"""Extract the structured 'result' dict. This is what API consumers (Synap, WSIT) read."""
|
||||
loc = location or DEFAULT_LOCATION
|
||||
current = weather["current"]
|
||||
daily = weather["daily"]
|
||||
condition = WMO_CODES.get(current["weather_code"], "Unknown")
|
||||
|
||||
forecast = []
|
||||
for i in range(len(daily["time"])):
|
||||
forecast.append({
|
||||
"date": daily["time"][i],
|
||||
"weekday": DAY_NAMES[datetime.strptime(daily["time"][i], "%Y-%m-%d").weekday()],
|
||||
"condition": WMO_CODES.get(daily["weather_code"][i], "Unknown"),
|
||||
"weather_code": daily["weather_code"][i],
|
||||
"temp_high_f": round(daily["temperature_2m_max"][i]),
|
||||
"temp_low_f": round(daily["temperature_2m_min"][i]),
|
||||
"precip_in": daily["precipitation_sum"][i],
|
||||
"wind_max_mph": round(daily["wind_speed_10m_max"][i]),
|
||||
"sunrise": daily["sunrise"][i],
|
||||
"sunset": daily["sunset"][i],
|
||||
})
|
||||
|
||||
return {
|
||||
"location": {
|
||||
"name": loc.get("name", ""),
|
||||
"state": loc.get("state", ""),
|
||||
"country": loc.get("country", ""),
|
||||
"lat": loc["lat"],
|
||||
"lon": loc["lon"],
|
||||
"label": location_label(loc),
|
||||
},
|
||||
"current": {
|
||||
"condition": condition,
|
||||
"weather_code": current["weather_code"],
|
||||
"temperature_f": round(current["temperature_2m"]),
|
||||
"feels_like_f": round(current["apparent_temperature"]),
|
||||
"wind_mph": round(current["wind_speed_10m"]),
|
||||
"humidity_pct": current["relative_humidity_2m"],
|
||||
},
|
||||
"forecast": forecast,
|
||||
"fetched_at": datetime.now(MT).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def run_structured(location=None):
|
||||
"""Fetch weather and return {result, markdown, summary}. This is the new contract —
|
||||
structured data for API consumers, markdown for the wiki."""
|
||||
loc = location or DEFAULT_LOCATION
|
||||
weather = fetch_weather(loc)
|
||||
section, summary = format_section(weather, loc)
|
||||
log_run(AGENT_ID, "success", output=summary, metadata={"location": location_label(loc)})
|
||||
return section, summary
|
||||
result = build_result(weather, loc)
|
||||
return {"result": result, "markdown": section, "summary": summary}
|
||||
|
||||
|
||||
def run(location=None):
|
||||
"""Legacy entrypoint: returns (markdown_section, summary). Kept for backward compat
|
||||
with daily_briefing.py's older call sites. Prefer run_structured().
|
||||
Deliberately does NOT call log_run — logging is the caller's responsibility.
|
||||
"""
|
||||
out = run_structured(location)
|
||||
return out["markdown"], out["summary"]
|
||||
|
||||
|
||||
def _location_from_config(cfg):
|
||||
"""Map an instance config dict to a location dict. Config keys: name, state, country, lat, lon."""
|
||||
if not cfg or "lat" not in cfg or "lon" not in cfg:
|
||||
return DEFAULT_LOCATION
|
||||
return {
|
||||
"name": cfg.get("name", "Custom"),
|
||||
"state": cfg.get("state", ""),
|
||||
"country": cfg.get("country", "US"),
|
||||
"lat": cfg["lat"],
|
||||
"lon": cfg["lon"],
|
||||
}
|
||||
|
||||
|
||||
def _main_from_api():
|
||||
"""Entry point when invoked via /api/instances/{id}/trigger. Reads INSTANCE_ID + RUN_ID
|
||||
from env, fetches config, runs, and posts structured result back to the dashboard."""
|
||||
instance_id = int(os.environ["INSTANCE_ID"])
|
||||
try:
|
||||
cfg = get_instance_config(instance_id)
|
||||
loc = _location_from_config(cfg)
|
||||
out = run_structured(loc)
|
||||
log_run(AGENT_ID, "success", output=out["markdown"],
|
||||
result=out["result"],
|
||||
metadata={"location": out["result"]["location"]["label"], "summary": out["summary"]})
|
||||
print(out["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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Weather Agent")
|
||||
parser.add_argument("--from-api", action="store_true",
|
||||
help="Run from /trigger: read INSTANCE_ID/RUN_ID from env, fetch config, post result")
|
||||
parser.add_argument("--lat", type=float, help="Latitude")
|
||||
parser.add_argument("--lon", type=float, help="Longitude")
|
||||
parser.add_argument("--name", type=str, help="City/location name")
|
||||
@@ -135,6 +224,10 @@ if __name__ == "__main__":
|
||||
parser.add_argument("--country", type=str, default="US", help="Country code")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.from_api:
|
||||
_main_from_api()
|
||||
sys.exit(0)
|
||||
|
||||
loc = DEFAULT_LOCATION
|
||||
if args.lat and args.lon:
|
||||
loc = {
|
||||
@@ -146,9 +239,9 @@ if __name__ == "__main__":
|
||||
}
|
||||
|
||||
try:
|
||||
section, summary = run(loc)
|
||||
print(section)
|
||||
print(f"\nSummary: {summary}")
|
||||
out = run_structured(loc)
|
||||
print(out["markdown"])
|
||||
print(f"\nSummary: {out['summary']}")
|
||||
except Exception as e:
|
||||
err_msg = f"{type(e).__name__}: {e}"
|
||||
print(f"Error: {err_msg}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user