Spaces:
Sleeping
Sleeping
| """Agent (Cursor) schedule endpoints using Bearer AGENT_TOKEN or session. | |
| GET context packs priors, risks, and constraints; PUT replaces the day plan. | |
| """ | |
| from __future__ import annotations | |
| import hmac | |
| from datetime import date, datetime, timedelta, timezone | |
| from typing import Any | |
| from fastapi import APIRouter, Depends, Request, status | |
| from app.deps import require_login | |
| from app.models import ApiEnvelope, err, ok | |
| from app.routers.loop import loop_summary_for | |
| from app.schedule_math import capacity_hint, trigger_operators | |
| from app.schedule_risk import risk_from_stores | |
| from app.schedule_store import DayPlanPut | |
| router = APIRouter(prefix="/api/agent", tags=["agent"]) | |
| def require_agent_or_login(request: Request) -> str: | |
| """Allow AGENT_TOKEN bearer or a valid human session.""" | |
| settings = request.app.state.settings | |
| auth = request.headers.get("authorization") or "" | |
| if auth.lower().startswith("bearer ") and settings.agent_token: | |
| token = auth[7:].strip() | |
| if hmac.compare_digest(token, settings.agent_token): | |
| return "agent" | |
| # Fall back to session login | |
| require_login(request) | |
| return "session" | |
| def agent_context( | |
| request: Request, | |
| day: date | None = None, | |
| _auth: str = Depends(require_agent_or_login), | |
| ) -> dict[str, Any]: | |
| settings = request.app.state.settings | |
| target = day or datetime.now(timezone.utc).date() | |
| day_s = target.isoformat() | |
| store = request.app.state.schedule_store | |
| entry_store = request.app.state.entry_store | |
| daily_store = request.app.state.daily_store | |
| now = datetime.now(timezone.utc) | |
| since = now - timedelta(hours=48) | |
| risk_tags_extra = {"home", "spain", "build"} | |
| entries_48h = [] | |
| for entry in entry_store._load(): | |
| ts = entry.ts if entry.ts.tzinfo else entry.ts.replace(tzinfo=timezone.utc) | |
| if ts < since: | |
| continue | |
| entries_48h.append( | |
| { | |
| "id": entry.id, | |
| "ts": entry.ts.isoformat(), | |
| "tags": entry.tags, | |
| "emotions": entry.emotions, | |
| "intensity": entry.intensity, | |
| "remedy": entry.remedy, | |
| "result": entry.result.value, | |
| "happened": entry.happened[:280], | |
| } | |
| ) | |
| risk = risk_from_stores(entry_store, daily_store, now=now, target_day=target) | |
| # Keep agent context tags slightly broader for Spain/home signals. | |
| tags_1h = sorted(set(risk["risk_1h_tags"])) | |
| for entry in entry_store._load(): | |
| ts = entry.ts if entry.ts.tzinfo else entry.ts.replace(tzinfo=timezone.utc) | |
| if ts < now - timedelta(hours=1): | |
| continue | |
| hit = sorted((set(entry.tags) | set(entry.emotions)) & risk_tags_extra) | |
| tags_1h = sorted(set(tags_1h) | set(hit)) | |
| risk_score = float(len(tags_1h)) | |
| last_hour_high = risk["last_hour_high"] or bool( | |
| {"urge", "court", "corn", "rerun"} & set(tags_1h) | |
| ) | |
| triggers_y = risk["triggers_yesterday"] | |
| cap = capacity_hint( | |
| risk_1h_score=risk_score, | |
| triggers_yesterday=triggers_y, | |
| yesterday_trigger=len(triggers_y) > 0, | |
| last_hour_high=last_hour_high, | |
| ) | |
| ops = trigger_operators(risk_1h_tags=tags_1h, triggers_yesterday=triggers_y) | |
| pending = sum(1 for e in entry_store._load() if e.result.value == "pending") | |
| priors = store.load_priors() | |
| y_daily = daily_store.get(target - timedelta(days=1)) | |
| t_daily = daily_store.get(target) | |
| movement = daily_store.movement_for(target) | |
| loop = loop_summary_for( | |
| entry_store=entry_store, | |
| daily_store=daily_store, | |
| as_of=target, | |
| days=7, | |
| ) | |
| return ok( | |
| { | |
| "date": day_s, | |
| "timezone": settings.schedule_timezone, | |
| "now": now.isoformat(), | |
| "daily_yesterday": y_daily.model_dump(mode="json") if y_daily else None, | |
| "daily_today": t_daily.model_dump(mode="json") if t_daily else None, | |
| "movement": movement, | |
| "indoors_streak": movement["indoors_streak"], | |
| "label_only_fail_rate": loop["label_only_fail_rate"], | |
| "loop_summary": loop, | |
| "entries_last_48h": entries_48h, | |
| "risk_1h": {"score": risk_score, "tags": tags_1h, "high": last_hour_high}, | |
| "triggers_yesterday": triggers_y, | |
| "capacity_hint": cap, | |
| "operators": ops, | |
| "priors": list(priors.values()), | |
| "server_priors_markdown": store.priors_table(), | |
| "current_plan": store.plan_with_feedback(day_s), | |
| "pending_entries_count": pending, | |
| "templates": store.templates(), | |
| "constraints": { | |
| "max_blocks": settings.schedule_max_blocks, | |
| "must_include_explore_or_restore": True, | |
| "p0_locked": True, | |
| "pad_with_d_hat": True, | |
| "day_start": settings.schedule_day_start, | |
| "day_end": settings.schedule_day_end, | |
| "scope": "P1_thin_loop", | |
| "no_perma_fields": True, | |
| "ev_formula_locked": True, | |
| "schedule_does_not_replace_daily_or_log": True, | |
| }, | |
| "formulas": ( | |
| "EV=P(done)*U+纬*I+畏*Fun-位t*d_hat-位m*cost-位f*FSE (locked; no 未/H/PERMA). " | |
| "d_hat=(n/(n+m))*mean_actual+(m/(n+m))*prior. " | |
| "On trigger_day: raise 位_f and 畏; P0 only overrides. " | |
| "Escape鈮爀xplore. Server owns p_helped/rank/picks from logs." | |
| ), | |
| } | |
| ) | |
| def agent_put_plan( | |
| request: Request, | |
| day: date, | |
| body: DayPlanPut, | |
| _auth: str = Depends(require_agent_or_login), | |
| ) -> object: | |
| put = body.model_copy(update={"source": body.source or "cursor"}) | |
| try: | |
| request.app.state.schedule_store.save_plan(day.isoformat(), put) | |
| return ok(request.app.state.schedule_store.plan_with_feedback(day.isoformat())) | |
| except ValueError as exc: | |
| return err("validation_error", str(exc), status.HTTP_422_UNPROCESSABLE_ENTITY) | |