feat(agents): stars agent — astrological transits via pyswisseph (#121)
Computes natal chart (Sun/Moon/Mercury/Venus/Mars/Jupiter/Saturn) from birth_date and finds active transits (conjunction/sextile/square/trine/ opposition) between today's sky and the user's natal positions. Top 3 most-exact transits are passed to the orchestrator as interpretive themes to colour the tip — grounded and actionable, not predictive. Birth date sourced from agent_prefs (populated by a connected Google data source); requires data:google-health consent. Agent self-silences when birth_date is absent. pyswisseph added to ml/serving/requirements.txt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ 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.stars import StarsAgent, _SWE_AVAILABLE
|
||||
from ml.agents.registry import get_agent, all_agents
|
||||
|
||||
_NOW = datetime(2026, 5, 1, 9, 0, 0, tzinfo=timezone.utc) # Thursday 09:00 UTC
|
||||
@@ -297,13 +298,56 @@ class TestTarotAgent:
|
||||
assert all(0 <= i < len(_CARDS) for i in indices)
|
||||
|
||||
|
||||
# ── StarsAgent ────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestStarsAgent:
|
||||
agent = StarsAgent()
|
||||
|
||||
def test_no_birth_date(self):
|
||||
out = self.agent.compute(_inp())
|
||||
_check_output(out, self.agent)
|
||||
assert out.signals_snapshot.get("no_birth_date") is True
|
||||
assert "birth date" in out.prompt_text.lower()
|
||||
|
||||
@pytest.mark.skipif(not _SWE_AVAILABLE, reason="pyswisseph not installed")
|
||||
def test_invalid_birth_date(self):
|
||||
out = self.agent.compute(_inp(agent_prefs={"birth_date": "not-a-date"}))
|
||||
_check_output(out, self.agent)
|
||||
assert out.signals_snapshot.get("invalid_birth_date") == "not-a-date"
|
||||
|
||||
@pytest.mark.skipif(not _SWE_AVAILABLE, reason="pyswisseph not installed")
|
||||
def test_with_birth_date(self):
|
||||
out = self.agent.compute(_inp(agent_prefs={"birth_date": "1990-06-15"}))
|
||||
_check_output(out, self.agent)
|
||||
assert "natal" in out.prompt_text.lower()
|
||||
assert out.signals_snapshot["birth_date"] == "1990-06-15"
|
||||
assert "natal_sun" in out.signals_snapshot
|
||||
assert "natal_moon" in out.signals_snapshot
|
||||
|
||||
@pytest.mark.skipif(not _SWE_AVAILABLE, reason="pyswisseph not installed")
|
||||
def test_transit_snapshot_structure(self):
|
||||
out = self.agent.compute(_inp(agent_prefs={"birth_date": "1985-03-21"}))
|
||||
snap = out.signals_snapshot
|
||||
assert "active_transits" in snap
|
||||
for t in snap["active_transits"]:
|
||||
assert {"transit_planet", "natal_planet", "aspect", "nature", "orb"} <= t.keys()
|
||||
|
||||
def test_swe_unavailable_path(self, monkeypatch):
|
||||
import ml.agents.stars as stars_mod
|
||||
monkeypatch.setattr(stars_mod, "_SWE_AVAILABLE", False)
|
||||
agent = StarsAgent()
|
||||
out = agent.compute(_inp(agent_prefs={"birth_date": "1990-06-15"}))
|
||||
_check_output(out, agent)
|
||||
assert out.signals_snapshot.get("swe_unavailable") is True
|
||||
|
||||
|
||||
# ── 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", "tarot"}
|
||||
assert ids == {"overdue-task", "momentum", "time-of-day", "recent-patterns", "focus-area", "health-vitals", "tarot", "stars"}
|
||||
|
||||
def test_get_agent(self):
|
||||
a = get_agent("momentum")
|
||||
|
||||
Reference in New Issue
Block a user