feat(agents): tarot agent — daily three-card draw (situation/action/outcome) (#120)
Draws 3 Major Arcana cards from a daily seed (user_id + date) so the reading is stable within a day and unique per user. Card meanings and action hints are precomputed in the agent; the orchestrator receives a structured prompt snippet and is instructed to weave the themes into a grounded, practical tip without explaining the cards. No inferred params, no external data — requires only data:core consent. TTL 6 h (refreshes at most twice daily). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ from ml.agents.momentum import MomentumAgent
|
||||
from ml.agents.time_of_day import TimeOfDayAgent
|
||||
from ml.agents.recent_patterns import RecentPatternsAgent
|
||||
from ml.agents.focus_area import FocusAreaAgent
|
||||
from ml.agents.tarot import TarotAgent, _daily_draw, _CARDS, _POSITIONS
|
||||
from ml.agents.registry import get_agent, all_agents
|
||||
|
||||
_NOW = datetime(2026, 5, 1, 9, 0, 0, tzinfo=timezone.utc) # Thursday 09:00 UTC
|
||||
@@ -250,13 +251,59 @@ class TestFocusAreaAgent:
|
||||
assert all("label" in c and "task_count" in c and "tasks" in c for c in clusters)
|
||||
|
||||
|
||||
# ── TarotAgent ────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestTarotAgent:
|
||||
agent = TarotAgent()
|
||||
|
||||
def test_basic_output(self):
|
||||
out = self.agent.compute(_inp())
|
||||
_check_output(out, self.agent)
|
||||
assert "situation" in out.prompt_text.lower()
|
||||
assert "action" in out.prompt_text.lower()
|
||||
assert "outcome" in out.prompt_text.lower()
|
||||
assert out.signals_snapshot["date"] == "2026-05-01"
|
||||
assert len(out.signals_snapshot["reading"]) == 3
|
||||
|
||||
def test_three_distinct_cards(self):
|
||||
out = self.agent.compute(_inp())
|
||||
cards = [r["card"] for r in out.signals_snapshot["reading"]]
|
||||
assert len(set(cards)) == 3
|
||||
|
||||
def test_positions_labelled(self):
|
||||
out = self.agent.compute(_inp())
|
||||
positions = [r["position"] for r in out.signals_snapshot["reading"]]
|
||||
assert positions == list(_POSITIONS)
|
||||
|
||||
def test_daily_stability(self):
|
||||
out1 = self.agent.compute(_inp(now=datetime(2026, 5, 1, 8, 0, 0, tzinfo=timezone.utc)))
|
||||
out2 = self.agent.compute(_inp(now=datetime(2026, 5, 1, 20, 0, 0, tzinfo=timezone.utc)))
|
||||
assert out1.signals_snapshot["reading"] == out2.signals_snapshot["reading"]
|
||||
|
||||
def test_different_days_different_draw(self):
|
||||
out1 = self.agent.compute(_inp(now=datetime(2026, 5, 1, 9, 0, 0, tzinfo=timezone.utc)))
|
||||
out2 = self.agent.compute(_inp(now=datetime(2026, 5, 2, 9, 0, 0, tzinfo=timezone.utc)))
|
||||
assert out1.signals_snapshot["reading"] != out2.signals_snapshot["reading"]
|
||||
|
||||
def test_different_users_different_draw(self):
|
||||
out1 = self.agent.compute(_inp(user_id="user-A"))
|
||||
out2 = self.agent.compute(_inp(user_id="user-B"))
|
||||
assert out1.signals_snapshot["reading"] != out2.signals_snapshot["reading"]
|
||||
|
||||
def test_daily_draw_returns_valid_indices(self):
|
||||
indices = _daily_draw("u1", "2026-05-01")
|
||||
assert len(indices) == 3
|
||||
assert len(set(indices)) == 3
|
||||
assert all(0 <= i < len(_CARDS) for i in indices)
|
||||
|
||||
|
||||
# ── Registry ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestRegistry:
|
||||
def test_all_agents_present(self):
|
||||
agents = all_agents()
|
||||
ids = {a.agent_id for a in agents}
|
||||
assert ids == {"overdue-task", "momentum", "time-of-day", "recent-patterns", "focus-area", "health-vitals"}
|
||||
assert ids == {"overdue-task", "momentum", "time-of-day", "recent-patterns", "focus-area", "health-vitals", "tarot"}
|
||||
|
||||
def test_get_agent(self):
|
||||
a = get_agent("momentum")
|
||||
|
||||
Reference in New Issue
Block a user