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>
75 lines
2.7 KiB
Python
75 lines
2.7 KiB
Python
from __future__ import annotations
|
|
from typing import ClassVar
|
|
from .base import BaseAgent, AgentInput, AgentOutput
|
|
from .manifest import AgentManifest
|
|
|
|
|
|
MANIFEST = AgentManifest(
|
|
id="momentum",
|
|
version="1.0.0",
|
|
description="Characterises the user's recent engagement trend from profile features.",
|
|
pref_schema={
|
|
"type": "object",
|
|
"additionalProperties": False,
|
|
"properties": {
|
|
"low_engagement_threshold_pct": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"maximum": 100,
|
|
"default": 25,
|
|
"description": "Completion rate below which momentum hints at low engagement.",
|
|
},
|
|
},
|
|
},
|
|
context_schema=["profile.features"],
|
|
required_consents=["data:core", "agent:momentum"],
|
|
output_contract={"type": "snippet", "format": "free_text"},
|
|
ttl_sec=21_600,
|
|
)
|
|
|
|
|
|
class MomentumAgent(BaseAgent):
|
|
"""Characterises the user's recent engagement trend from profile features."""
|
|
agent_id: ClassVar[str] = MANIFEST.id
|
|
ttl_seconds: ClassVar[int] = MANIFEST.ttl_sec
|
|
version: ClassVar[str] = MANIFEST.version
|
|
|
|
def compute(self, inp: AgentInput) -> AgentOutput:
|
|
completion = inp.profile.get("completion_rate_30d")
|
|
dismiss = inp.profile.get("dismiss_rate_30d")
|
|
volume = inp.profile.get("tip_volume_30d")
|
|
|
|
parts: list[str] = []
|
|
|
|
if completion is not None:
|
|
pct = round(completion * 100)
|
|
if pct >= 50:
|
|
parts.append(f"The user completes {pct}% of tips (strong engagement).")
|
|
elif pct >= 25:
|
|
parts.append(f"The user completes {pct}% of tips (moderate engagement).")
|
|
else:
|
|
parts.append(
|
|
f"The user completes {pct}% of tips "
|
|
f"(low engagement — prefer simple, immediately actionable tips)."
|
|
)
|
|
else:
|
|
parts.append("No completion-rate data yet (new user).")
|
|
|
|
if dismiss is not None:
|
|
dpct = round(dismiss * 100)
|
|
if dpct >= 40:
|
|
parts.append(f"Dismiss rate is high ({dpct}%) — avoid repetitive or irrelevant tips.")
|
|
elif dpct <= 10:
|
|
parts.append(f"Dismiss rate is low ({dpct}%).")
|
|
|
|
if volume is not None and int(volume) < 5:
|
|
parts.append("Very few tips served so far — this is an early-stage user.")
|
|
|
|
prompt = " ".join(parts) if parts else "No engagement data available yet."
|
|
snapshot = {
|
|
"completion_rate_30d": completion,
|
|
"dismiss_rate_30d": dismiss,
|
|
"tip_volume_30d": volume,
|
|
}
|
|
return self._make_output(inp, prompt, snapshot)
|