Spaces:
Sleeping
Sleeping
File size: 6,182 Bytes
c6253b2 57ed4c2 c6253b2 9af2b22 c6253b2 9af2b22 c6253b2 9af2b22 c6253b2 9af2b22 c6253b2 9af2b22 c6253b2 9af2b22 4c23c1f 57ed4c2 c6253b2 4c23c1f 57ed4c2 c6253b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | """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"
@router.get("/context", response_model=ApiEnvelope)
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."
),
}
)
@router.put("/plan/{day}", response_model=ApiEnvelope)
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)
|