Each agent now exports a module-level MANIFEST declaring id, version, pref_schema, required_consents, ttl_sec, and silenced_in_contexts. The registry surfaces both the agent and its manifest, and rejects on mismatch so the two cannot drift. ml/serving exposes GET /agents/registry; services/api proxies it as GET /api/agents/registry with a 60s in-process cache so admin pageviews don't hammer upstream. Failures aren't cached. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
from __future__ import annotations
|
|
from typing import ClassVar
|
|
from .base import BaseAgent, AgentInput, AgentOutput
|
|
from .manifest import AgentManifest
|
|
|
|
_DOW_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
|
|
|
|
|
MANIFEST = AgentManifest(
|
|
id="time-of-day",
|
|
version="1.0.0",
|
|
description="Frames the current moment relative to the user's productive peak and quiet hours.",
|
|
pref_schema={
|
|
"type": "object",
|
|
"additionalProperties": False,
|
|
"properties": {
|
|
"quiet_start": {
|
|
"type": "string",
|
|
"pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
|
|
"description": "HH:MM start of quiet hours (24h, user's local TZ).",
|
|
},
|
|
"quiet_end": {
|
|
"type": "string",
|
|
"pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
|
|
"description": "HH:MM end of quiet hours.",
|
|
},
|
|
},
|
|
},
|
|
context_schema=["profile.features"],
|
|
required_consents=["data:core", "agent:time-of-day"],
|
|
output_contract={"type": "snippet", "format": "free_text"},
|
|
ttl_sec=900,
|
|
)
|
|
|
|
|
|
class TimeOfDayAgent(BaseAgent):
|
|
"""Frames the current moment relative to the user's productive peak."""
|
|
agent_id: ClassVar[str] = MANIFEST.id
|
|
ttl_seconds: ClassVar[int] = MANIFEST.ttl_sec
|
|
version: ClassVar[str] = MANIFEST.version
|
|
|
|
def compute(self, inp: AgentInput) -> AgentOutput:
|
|
hour = inp.now.hour
|
|
dow = inp.now.weekday() # 0=Monday … 6=Sunday
|
|
preferred = inp.profile.get("preferred_hour")
|
|
is_weekend = dow >= 5
|
|
|
|
parts = [f"It is {hour:02d}:00 on {_DOW_NAMES[dow]} ({self._label(hour)})."]
|
|
|
|
if is_weekend:
|
|
parts.append("Weekend context — prefer personal or reflective tips over work tasks.")
|
|
|
|
if preferred is not None:
|
|
ph = int(preferred)
|
|
delta = min(abs(hour - ph), 24 - abs(hour - ph)) # circular distance
|
|
if delta == 0:
|
|
parts.append(
|
|
f"This is the user's peak productivity hour ({ph:02d}:00) — "
|
|
f"a high-impact tip is appropriate."
|
|
)
|
|
elif delta <= 2:
|
|
parts.append(f"Approaching the user's peak productivity window ({ph:02d}:00).")
|
|
else:
|
|
parts.append("No preferred-hour data yet.")
|
|
|
|
prompt = " ".join(parts)
|
|
snapshot = {"hour": hour, "day_of_week": dow, "preferred_hour": preferred}
|
|
return self._make_output(inp, prompt, snapshot)
|
|
|
|
@staticmethod
|
|
def _label(hour: int) -> str:
|
|
if 5 <= hour < 12:
|
|
return "morning"
|
|
if 12 <= hour < 17:
|
|
return "afternoon"
|
|
if 17 <= hour < 21:
|
|
return "evening"
|
|
return "night"
|