test_step4_local_astro.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. """
  2. Step 4: Call the LOCAL astro-mcp server via MCP SSE.
  3. Tests both paths (DB lookup + direct call) and compares angles against ground truth.
  4. """
  5. from __future__ import annotations
  6. import asyncio
  7. import json
  8. import os
  9. from datetime import timedelta
  10. from mcp import ClientSession
  11. from mcp.client.sse import sse_client
  12. # ── Ground truth from Step 1 (Placidus, Swiss Ephemeris direct) ─────
  13. REFERENCE = {
  14. "trump": {
  15. "label": "Donald Trump",
  16. "birth_datetime": "1946-06-14T10:54:00-04:00",
  17. "lat": 40.72677,
  18. "lon": -73.74152,
  19. # Ground truth: swe.houses(jd, 40.72677, -73.74152, b'P')
  20. "asc": 150.025101,
  21. "mc": 54.423678,
  22. },
  23. "einstein": {
  24. "label": "Albert Einstein",
  25. "birth_datetime": "1879-03-14T11:30:00+00:53",
  26. "lat": 48.39841,
  27. "lon": 9.99155,
  28. "asc": 98.923507,
  29. "mc": 339.337618,
  30. },
  31. "chaka": {
  32. "label": "Chaka Khan",
  33. "birth_datetime": "1953-03-23T21:05:00-06:00",
  34. "lat": 41.87811,
  35. "lon": -87.62980,
  36. "asc": 218.923115,
  37. "mc": 137.470885,
  38. },
  39. }
  40. TOLERANCE = 0.01
  41. ASTRO_URL = os.environ.get("ASTRO_MCP_URL", "http://127.0.0.1:7016/mcp/sse")
  42. def _payload(result):
  43. p = getattr(result, "structuredContent", None)
  44. if isinstance(p, dict):
  45. return p
  46. for item in getattr(result, "content", []) or []:
  47. t = getattr(item, "text", None)
  48. if isinstance(t, str) and t.strip():
  49. try:
  50. d = json.loads(t)
  51. if isinstance(d, dict):
  52. return d
  53. except Exception:
  54. continue
  55. return {}
  56. def _fmt(lon):
  57. signs = ["Ari", "Tau", "Gem", "Can", "Leo", "Vir",
  58. "Lib", "Sco", "Sag", "Cap", "Aqu", "Pis"]
  59. idx = int(lon // 30) % 12
  60. deg = lon % 30.0
  61. return f"{signs[idx]} {deg:05.2f} (abs {lon:.6f})"
  62. def check(ref, angles, houses, path_label):
  63. asc = angles.get("ascendant", {}).get("absolute_lon")
  64. mc = angles.get("midheaven", {}).get("absolute_lon")
  65. h1 = houses[0]["absolute_lon"] if houses else None
  66. asc_diff = abs(asc - ref["asc"]) if asc else float("inf")
  67. mc_diff = abs(mc - ref["mc"]) if mc else float("inf")
  68. asc_ok = asc_diff < TOLERANCE
  69. mc_ok = mc_diff < TOLERANCE
  70. print(f"\n [{path_label}]")
  71. if asc:
  72. print(f" ASC: {_fmt(asc)} exp: {_fmt(ref['asc'])} diff={asc_diff:.6f} {'OK' if asc_ok else 'MISMATCH'}")
  73. else:
  74. print(f" ASC: MISSING")
  75. if mc:
  76. print(f" MC: {_fmt(mc)} exp: {_fmt(ref['mc'])} diff={mc_diff:.6f} {'OK' if mc_ok else 'MISMATCH'}")
  77. else:
  78. print(f" MC: MISSING")
  79. if not asc_ok or not mc_ok:
  80. print(f" FULL ANGLES: {json.dumps(angles, indent=6, default=str)[:600]}")
  81. return asc_ok and mc_ok
  82. async def main():
  83. print("=" * 70)
  84. print("STEP 4: Local astro-mcp — find where angles get corrupted")
  85. print(f" URL: {ASTRO_URL}")
  86. print("=" * 70)
  87. url = ASTRO_URL
  88. if not url.endswith("/mcp/sse"):
  89. url = url.rstrip("/") + "/mcp/sse"
  90. all_ok = True
  91. async with sse_client(url, timeout=30, sse_read_timeout=30) as streams:
  92. async with ClientSession(*streams, read_timeout_seconds=timedelta(seconds=30)) as session:
  93. await session.initialize()
  94. for key, ref in REFERENCE.items():
  95. print(f"\n{'='*50}")
  96. print(f" {ref['label']}")
  97. print(f" Input: {ref['birth_datetime']} lat={ref['lat']} lon={ref['lon']}")
  98. # Path A: DB lookup
  99. chart_a = _payload(await session.call_tool(
  100. "calculate_natal_chart_by_id",
  101. {"person_id": key, "house_system": "placidus"},
  102. ))
  103. if "error" in chart_a:
  104. print(f"\n [DB PATH] ERROR: {chart_a['error']}")
  105. all_ok = False
  106. else:
  107. ok = check(ref, chart_a.get("angles", {}), chart_a.get("houses", []), "DB by_id")
  108. if not ok:
  109. all_ok = False
  110. # Path B: Direct call
  111. chart_b = _payload(await session.call_tool(
  112. "calculate_natal_chart",
  113. {
  114. "birth_datetime": ref["birth_datetime"],
  115. "latitude": ref["lat"],
  116. "longitude": ref["lon"],
  117. "house_system": "placidus",
  118. },
  119. ))
  120. if "error" in chart_b:
  121. print(f"\n [DIRECT PATH] ERROR: {chart_b['error']}")
  122. all_ok = False
  123. else:
  124. ok = check(ref, chart_b.get("angles", {}), chart_b.get("houses", []), "DIRECT")
  125. if not ok:
  126. all_ok = False
  127. print("\n" + "=" * 70)
  128. if all_ok:
  129. print("RESULT: LOCAL ASTRO-MCP IS CORRECT")
  130. else:
  131. print("RESULT: LOCAL ASTRO-MCP HAS MISMATCHES")
  132. print("=" * 70)
  133. if __name__ == "__main__":
  134. asyncio.run(main())