feat(profile): /api/profile + eligibility filter + inference framework (ADR-0014 steps 4-6)
Step 4 — /api/profile read-through API:
GET /api/profile → { user, prefs, consents, contexts }
PATCH /api/profile/prefs/:scope upsert user_preferences (source='user')
PATCH /api/profile/consents grant / revoke consent keys
PATCH /api/profile/contexts create / activate / deactivate contexts
Legacy consentGiven bit folded in as data:core fallback.
Step 5 — registry-driven eligibility filter:
fetchRegistry() exported from agent-registry.ts.
profile/eligibility.ts: getEligibleAgentIds(userId) — filters by required
consents, silenced_in_contexts, and user_preferences[enabled=false].
fetchOrchestratorTip filters agent_outputs to eligible set before calling
ml/serving /recommend. Fail-closed: registry unavailable → empty set.
Step 6 — shared context-inference framework (#111) + time-of-day proof (#112):
ml/agents/inference/: UserHistory, FeedbackEvent, run_inference().
Framework: cold-start, min_history gating, error fallback, structured logs.
TimeOfDayAgent v1.1.0: inferred_params=[preferred_hour]; also reads
quiet_start/quiet_end from agent_prefs. agent_prefs injected by TS caller.
AgentInput gains agent_prefs field.
ml/serving: POST /agents/{agent_id}/infer endpoint.
agent-outputs.ts computeAndStore: loads prefs before compute, calls /infer
after, persists results (source='inferred'); user overrides never touched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
9
ml/agents/inference/__init__.py
Normal file
9
ml/agents/inference/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Shared context-inference framework (ADR-0014 §3, issue #111).
|
||||
|
||||
Each agent's manifest declares InferredParams; this package owns the
|
||||
scheduling contract, history data model, and write path to user_preferences.
|
||||
"""
|
||||
from .framework import run_inference
|
||||
from .history import FeedbackEvent, UserHistory
|
||||
|
||||
__all__ = ["run_inference", "FeedbackEvent", "UserHistory"]
|
||||
59
ml/agents/inference/framework.py
Normal file
59
ml/agents/inference/framework.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""run_inference — core of the context-inference framework (ADR-0014 §3).
|
||||
|
||||
Contract:
|
||||
run_inference(manifest, history) → dict[key, value]
|
||||
|
||||
Semantics:
|
||||
- For each InferredParam in manifest.inferred_params:
|
||||
- If len(history.events) < param.min_history → emit cold_start_default.
|
||||
- Otherwise → call param.infer(history) and emit the result.
|
||||
- Returns {key: value} ready for the caller to persist to user_preferences
|
||||
with source='inferred'.
|
||||
- User overrides (source='user') are handled by the caller's upsert logic;
|
||||
this function has no DB access.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from ..manifest import AgentManifest
|
||||
from .history import UserHistory
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_inference(manifest: AgentManifest, history: UserHistory) -> dict[str, Any]:
|
||||
"""Evaluate all InferredParams for an agent and return {key: inferred_value}."""
|
||||
result: dict[str, Any] = {}
|
||||
n = len(history.events)
|
||||
|
||||
for param in manifest.inferred_params:
|
||||
t0 = time.monotonic()
|
||||
if param.infer is None:
|
||||
result[param.key] = param.cold_start_default
|
||||
continue
|
||||
if n < param.min_history:
|
||||
value = param.cold_start_default
|
||||
source = "cold_start"
|
||||
else:
|
||||
try:
|
||||
value = param.infer(history)
|
||||
source = "inferred"
|
||||
except Exception as exc:
|
||||
log.warning(
|
||||
"inference_error agent=%s param=%s error=%s — using cold_start_default",
|
||||
manifest.id, param.key, exc,
|
||||
)
|
||||
value = param.cold_start_default
|
||||
source = "error_fallback"
|
||||
|
||||
latency_ms = round((time.monotonic() - t0) * 1000, 1)
|
||||
log.info(
|
||||
"inference_param agent=%s param=%s source=%s value=%r history_len=%d latency_ms=%s",
|
||||
manifest.id, param.key, source, value, n, latency_ms,
|
||||
)
|
||||
result[param.key] = value
|
||||
|
||||
return result
|
||||
29
ml/agents/inference/history.py
Normal file
29
ml/agents/inference/history.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""UserHistory — normalised view of a user's feedback events for inference."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
@dataclass
|
||||
class FeedbackEvent:
|
||||
action: str # 'done' | 'dismiss' | 'snooze' | 'helpful' | 'not_helpful'
|
||||
dwell_ms: int | None
|
||||
created_at: str # ISO 8601
|
||||
|
||||
@property
|
||||
def hour(self) -> int:
|
||||
"""Hour of day (0-23) when the feedback was recorded."""
|
||||
try:
|
||||
dt = datetime.fromisoformat(self.created_at.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return 12
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.hour
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserHistory:
|
||||
user_id: str
|
||||
events: list[FeedbackEvent] = field(default_factory=list)
|
||||
Reference in New Issue
Block a user