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:
110
ml/agents/tarot.py
Normal file
110
ml/agents/tarot.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""TAROT agent — three-card draw (situation / action / outcome).
|
||||
|
||||
Draws cards deterministically from a daily seed so the reading stays
|
||||
stable for the day (same cards whether the agent runs at 08:00 or 14:00).
|
||||
Card meanings are precomputed here and passed as a structured snippet to
|
||||
the orchestrator, which weaves them into a grounded, actionable tip.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import ClassVar
|
||||
|
||||
from .base import BaseAgent, AgentInput, AgentOutput
|
||||
from .manifest import AgentManifest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Card definitions — Major Arcana only (22 cards, indices 0–21)
|
||||
# Each entry: (name, upright_meaning, action_hint)
|
||||
# ---------------------------------------------------------------------------
|
||||
_CARDS: list[tuple[str, str, str]] = [
|
||||
("The Fool", "new beginnings, spontaneity, a leap of faith", "start something without overthinking"),
|
||||
("The Magician", "skill, willpower, resourcefulness", "use what you already have"),
|
||||
("The High Priestess","intuition, inner knowing, patience", "listen to what you already sense is true"),
|
||||
("The Empress", "abundance, creativity, nurturing", "invest energy in something generative"),
|
||||
("The Emperor", "structure, authority, discipline", "set a boundary or impose order"),
|
||||
("The Hierophant", "tradition, guidance, shared values", "seek or offer mentorship"),
|
||||
("The Lovers", "alignment, choice, commitment", "make a decision you have been avoiding"),
|
||||
("The Chariot", "determination, focus, forward motion", "push through the resistance"),
|
||||
("Strength", "inner courage, patience, gentle persistence", "stay the course with compassion"),
|
||||
("The Hermit", "solitude, reflection, inner guidance", "step back and think before acting"),
|
||||
("Wheel of Fortune", "cycles, turning points, inevitable change", "acknowledge what is shifting around you"),
|
||||
("Justice", "fairness, truth, cause and effect", "audit a recent decision for its real consequences"),
|
||||
("The Hanged Man", "pause, surrender, new perspective", "release your grip on the outcome"),
|
||||
("Death", "endings, transformation, release", "let go of what no longer serves you"),
|
||||
("Temperance", "balance, moderation, patience", "blend two competing demands"),
|
||||
("The Devil", "attachment, habit, shadow patterns", "name a loop you are stuck in"),
|
||||
("The Tower", "sudden disruption, revelation, necessary collapse", "accept the thing that already broke"),
|
||||
("The Star", "hope, renewal, calm after the storm", "trust that recovery is already underway"),
|
||||
("The Moon", "uncertainty, illusion, the unconscious", "sit with ambiguity rather than forcing clarity"),
|
||||
("The Sun", "clarity, vitality, success", "act from your most energised self"),
|
||||
("Judgement", "reflection, reckoning, a call to rise", "respond to a long-deferred summons"),
|
||||
("The World", "completion, integration, a cycle closing", "acknowledge what you have finished"),
|
||||
]
|
||||
|
||||
_POSITIONS = ("situation", "action", "outcome")
|
||||
|
||||
|
||||
def _daily_draw(user_id: str, date_str: str) -> list[int]:
|
||||
"""Return three distinct card indices seeded by (user_id, date)."""
|
||||
seed = hashlib.sha256(f"{user_id}:{date_str}".encode()).digest()
|
||||
indices: list[int] = []
|
||||
offset = 0
|
||||
while len(indices) < 3:
|
||||
val = int.from_bytes(seed[offset:offset + 2], "big") % len(_CARDS)
|
||||
if val not in indices:
|
||||
indices.append(val)
|
||||
offset = (offset + 2) % (len(seed) - 1)
|
||||
return indices
|
||||
|
||||
|
||||
MANIFEST = AgentManifest(
|
||||
id="tarot",
|
||||
version="1.0.0",
|
||||
description="Daily three-card draw (situation/action/outcome) that frames the tip as a symbolic reflection.",
|
||||
pref_schema={
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Set false to disable the tarot agent for this user.",
|
||||
},
|
||||
},
|
||||
},
|
||||
context_schema=[],
|
||||
required_consents=["data:core"],
|
||||
output_contract={"type": "snippet", "format": "free_text"},
|
||||
ttl_sec=3_600 * 6, # stable for 6 h; refreshes mid-day at most twice
|
||||
silenced_in_contexts=[],
|
||||
inferred_params=[],
|
||||
)
|
||||
|
||||
|
||||
class TarotAgent(BaseAgent):
|
||||
"""Produces a three-card reading as a prompt snippet."""
|
||||
agent_id: ClassVar[str] = MANIFEST.id
|
||||
ttl_seconds: ClassVar[int] = MANIFEST.ttl_sec
|
||||
version: ClassVar[str] = MANIFEST.version
|
||||
|
||||
def compute(self, inp: AgentInput) -> AgentOutput:
|
||||
date_str = inp.now.strftime("%Y-%m-%d")
|
||||
indices = _daily_draw(inp.user_id, date_str)
|
||||
|
||||
reading: list[dict] = []
|
||||
parts: list[str] = [f"Today's tarot reading ({date_str}):"]
|
||||
for pos, idx in zip(_POSITIONS, indices):
|
||||
name, meaning, hint = _CARDS[idx]
|
||||
reading.append({"position": pos, "card": name, "meaning": meaning, "hint": hint})
|
||||
parts.append(f" {pos.capitalize()} — {name}: {meaning}. Hint: {hint}.")
|
||||
|
||||
parts.append(
|
||||
"Weave these symbolic themes lightly into the tip — "
|
||||
"ground them in practical, specific action. "
|
||||
"Do not explain the cards; let their meaning shape the advice."
|
||||
)
|
||||
|
||||
prompt = "\n".join(parts)
|
||||
snapshot = {"date": date_str, "reading": reading}
|
||||
return self._make_output(inp, prompt, snapshot)
|
||||
Reference in New Issue
Block a user