feat(agents): per-agent inference — momentum, overdue-task, recent-patterns, focus-area (ADR-0014 step 7)
All four agents bumped to v1.1.0. momentum (#114): infers engagement_trend ('up'|'stable'|'down') by comparing done-rate in the last 7 days vs the prior 7 days. Agent surfaces the trend in its snippet ("trending up — build on the momentum"). overdue-task (#115): infers lateness_tolerance_days (0/1/2) from snooze rate. Agent now filters tasks against the tolerance so low-urgency users aren't nagged about tasks that are only hours overdue. recent-patterns (#116): infers window_days (7/14/30) from feedback event density — sparse users get a wider window so the snippet isn't always empty. focus-area (#113): no inferred params (project-level feedback linkage needed, tracked under #78). preferred_areas pref was declared but ignored; agent now honours it as a tiebreaker and mentions it in the snippet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import ClassVar
|
||||
|
||||
from .base import BaseAgent, AgentInput, AgentOutput
|
||||
from .manifest import AgentManifest
|
||||
from .inference.history import UserHistory
|
||||
from .manifest import AgentManifest, InferredParam
|
||||
|
||||
|
||||
def _infer_engagement_trend(history: UserHistory) -> str:
|
||||
"""Compare done-rate in the most recent 7 days vs the 7 days before that."""
|
||||
events = sorted(history.events, key=lambda e: e.created_at)
|
||||
if not events:
|
||||
return "stable"
|
||||
|
||||
try:
|
||||
latest = datetime.fromisoformat(events[-1].created_at.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return "stable"
|
||||
|
||||
cutoff_recent = latest - timedelta(days=7)
|
||||
cutoff_older = latest - timedelta(days=14)
|
||||
|
||||
recent = [e for e in events if _parse_dt(e.created_at) >= cutoff_recent]
|
||||
older = [e for e in events if cutoff_older <= _parse_dt(e.created_at) < cutoff_recent]
|
||||
|
||||
if len(older) < 3:
|
||||
return "stable" # not enough baseline to compare
|
||||
|
||||
recent_rate = sum(1 for e in recent if e.action == "done") / max(len(recent), 1)
|
||||
older_rate = sum(1 for e in older if e.action == "done") / max(len(older), 1)
|
||||
|
||||
delta = recent_rate - older_rate
|
||||
if delta > 0.10:
|
||||
return "up"
|
||||
if delta < -0.10:
|
||||
return "down"
|
||||
return "stable"
|
||||
|
||||
|
||||
def _parse_dt(iso: str) -> datetime:
|
||||
try:
|
||||
dt = datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt
|
||||
except ValueError:
|
||||
return datetime.min.replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
MANIFEST = AgentManifest(
|
||||
id="momentum",
|
||||
version="1.0.0",
|
||||
version="1.1.0", # bumped: engagement_trend InferredParam added (#114)
|
||||
description="Characterises the user's recent engagement trend from profile features.",
|
||||
pref_schema={
|
||||
"type": "object",
|
||||
@@ -25,6 +70,15 @@ MANIFEST = AgentManifest(
|
||||
required_consents=["data:core", "agent:momentum"],
|
||||
output_contract={"type": "snippet", "format": "free_text"},
|
||||
ttl_sec=21_600,
|
||||
inferred_params=[
|
||||
InferredParam(
|
||||
key="engagement_trend",
|
||||
ttl_sec=21_600, # recompute every 6 hours alongside snippet
|
||||
cold_start_default="stable",
|
||||
min_history=10,
|
||||
infer=_infer_engagement_trend,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -38,6 +92,7 @@ class MomentumAgent(BaseAgent):
|
||||
completion = inp.profile.get("completion_rate_30d")
|
||||
dismiss = inp.profile.get("dismiss_rate_30d")
|
||||
volume = inp.profile.get("tip_volume_30d")
|
||||
trend: str = inp.agent_prefs.get("engagement_trend", "stable")
|
||||
|
||||
parts: list[str] = []
|
||||
|
||||
@@ -65,10 +120,16 @@ class MomentumAgent(BaseAgent):
|
||||
if volume is not None and int(volume) < 5:
|
||||
parts.append("Very few tips served so far — this is an early-stage user.")
|
||||
|
||||
if trend == "up":
|
||||
parts.append("Engagement is trending up compared to last week — build on the momentum.")
|
||||
elif trend == "down":
|
||||
parts.append("Engagement is trending down — a motivational or easy-win tip may help.")
|
||||
|
||||
prompt = " ".join(parts) if parts else "No engagement data available yet."
|
||||
snapshot = {
|
||||
"completion_rate_30d": completion,
|
||||
"dismiss_rate_30d": dismiss,
|
||||
"tip_volume_30d": volume,
|
||||
"engagement_trend": trend,
|
||||
}
|
||||
return self._make_output(inp, prompt, snapshot)
|
||||
|
||||
Reference in New Issue
Block a user