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:
52
ml/serving/tests/test_infer_endpoint.py
Normal file
52
ml/serving/tests/test_infer_endpoint.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""POST /agents/{agent_id}/infer — inference framework endpoint."""
|
||||
import pytest
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
from main import app
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_infer_time_of_day_cold_start():
|
||||
"""Fewer than min_history events → cold_start_default for preferred_hour."""
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.post("/agents/time-of-day/infer", json={
|
||||
"user_id": "u1",
|
||||
"feedback_history": [
|
||||
{"action": "done", "dwell_ms": 60000, "created_at": "2026-05-01T09:00:00+00:00"},
|
||||
] * 5, # 5 < min_history=10
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["agent_id"] == "time-of-day"
|
||||
assert body["inferred_prefs"]["preferred_hour"] is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_infer_time_of_day_enough_history():
|
||||
"""10+ events → preferred_hour is inferred as the mode done-hour."""
|
||||
events = [{"action": "done", "dwell_ms": 60000, "created_at": "2026-05-01T09:00:00+00:00"}] * 10
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.post("/agents/time-of-day/infer", json={"user_id": "u1", "feedback_history": events})
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["inferred_prefs"]["preferred_hour"] == 9
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_infer_agent_with_no_inferred_params():
|
||||
"""Agents with no inferred_params return an empty dict."""
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.post("/agents/overdue-task/infer", json={"user_id": "u1", "feedback_history": []})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["inferred_prefs"] == {}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_infer_unknown_agent_404():
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.post("/agents/ghost/infer", json={"user_id": "u1", "feedback_history": []})
|
||||
assert resp.status_code == 404
|
||||
Reference in New Issue
Block a user