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>
59 lines
2.3 KiB
Python
59 lines
2.3 KiB
Python
"""Base class and shared data structures for all recommendation sub-agents."""
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import ClassVar
|
|
|
|
|
|
@dataclass
|
|
class AgentInput:
|
|
"""Everything an agent may need to produce its prompt snippet."""
|
|
user_id: str
|
|
tasks: list[dict] # task signal dicts (content, priority, is_overdue, …)
|
|
profile: dict[str, float | None] # profile feature values keyed by feature name
|
|
feedback_history: list[dict] = field(default_factory=list) # [{action, dwell_ms, created_at}, …]
|
|
now: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
# Per-agent inferred/user prefs loaded from user_preferences (ADR-0014 §3).
|
|
# Keys match the agent's pref_schema + inferred_params. 'user' source takes
|
|
# precedence over 'inferred' source; the caller resolves priority before
|
|
# passing this dict in.
|
|
agent_prefs: dict = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class AgentOutput:
|
|
"""Result produced by an agent; persisted to agent_outputs table."""
|
|
user_id: str
|
|
agent_id: str
|
|
prompt_text: str # snippet passed to the orchestrator
|
|
signals_snapshot: dict # inputs consumed (for explainability / debugging)
|
|
computed_at: str # ISO 8601
|
|
expires_at: str # ISO 8601
|
|
agent_version: str
|
|
|
|
|
|
class BaseAgent(ABC):
|
|
agent_id: ClassVar[str]
|
|
ttl_seconds: ClassVar[int]
|
|
version: ClassVar[str]
|
|
|
|
@abstractmethod
|
|
def compute(self, inp: AgentInput) -> AgentOutput:
|
|
"""Analyse inp and return a prompt snippet describing what was found."""
|
|
...
|
|
|
|
def _make_output(self, inp: AgentInput, prompt_text: str, snapshot: dict) -> AgentOutput:
|
|
computed_at = inp.now.astimezone(timezone.utc).isoformat()
|
|
expires_at = (inp.now.astimezone(timezone.utc) + timedelta(seconds=self.ttl_seconds)).isoformat()
|
|
return AgentOutput(
|
|
user_id=inp.user_id,
|
|
agent_id=self.agent_id,
|
|
prompt_text=prompt_text,
|
|
signals_snapshot=snapshot,
|
|
computed_at=computed_at,
|
|
expires_at=expires_at,
|
|
agent_version=self.version,
|
|
)
|