kundali-chart-mcp 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +67 -0
  2. package/azure-function/function_app.py +93 -0
  3. package/azure-function/host.json +15 -0
  4. package/azure-function/kundali_bridge.py +952 -0
  5. package/azure-function/python/kundali_lib/__init__.py +1 -0
  6. package/azure-function/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/azure-function/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
  8. package/azure-function/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
  9. package/azure-function/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
  10. package/azure-function/python/kundali_lib/ephemeris.py +30 -0
  11. package/azure-function/python/kundali_lib/geocoder.py +82 -0
  12. package/azure-function/python/kundali_lib/vedic/__init__.py +1 -0
  13. package/azure-function/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
  14. package/azure-function/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
  15. package/azure-function/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
  16. package/azure-function/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
  17. package/azure-function/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
  18. package/azure-function/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
  19. package/azure-function/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
  20. package/azure-function/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
  21. package/azure-function/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
  22. package/azure-function/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
  23. package/azure-function/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
  24. package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
  25. package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
  26. package/azure-function/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
  27. package/azure-function/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
  28. package/azure-function/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
  29. package/azure-function/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
  30. package/azure-function/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
  31. package/azure-function/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
  32. package/azure-function/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
  33. package/azure-function/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
  34. package/azure-function/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
  35. package/azure-function/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
  36. package/azure-function/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
  37. package/azure-function/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
  38. package/azure-function/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
  39. package/azure-function/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
  40. package/azure-function/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
  41. package/azure-function/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
  42. package/azure-function/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
  43. package/azure-function/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
  44. package/azure-function/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
  45. package/azure-function/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
  46. package/azure-function/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
  47. package/azure-function/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
  48. package/azure-function/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
  49. package/azure-function/python/kundali_lib/vedic/arishta.py +465 -0
  50. package/azure-function/python/kundali_lib/vedic/ashtakavarga.py +213 -0
  51. package/azure-function/python/kundali_lib/vedic/avasthas.py +292 -0
  52. package/azure-function/python/kundali_lib/vedic/ayanamsa.py +106 -0
  53. package/azure-function/python/kundali_lib/vedic/bhava_chalit.py +137 -0
  54. package/azure-function/python/kundali_lib/vedic/char_dasha.py +308 -0
  55. package/azure-function/python/kundali_lib/vedic/chart.py +126 -0
  56. package/azure-function/python/kundali_lib/vedic/chart_types.py +338 -0
  57. package/azure-function/python/kundali_lib/vedic/compatibility.py +705 -0
  58. package/azure-function/python/kundali_lib/vedic/constants.py +108 -0
  59. package/azure-function/python/kundali_lib/vedic/dasha_extended.py +262 -0
  60. package/azure-function/python/kundali_lib/vedic/dasha_systems.py +439 -0
  61. package/azure-function/python/kundali_lib/vedic/doshas.py +453 -0
  62. package/azure-function/python/kundali_lib/vedic/gandanta.py +213 -0
  63. package/azure-function/python/kundali_lib/vedic/gochara.py +277 -0
  64. package/azure-function/python/kundali_lib/vedic/hora.py +263 -0
  65. package/azure-function/python/kundali_lib/vedic/houses.py +30 -0
  66. package/azure-function/python/kundali_lib/vedic/jaimini.py +361 -0
  67. package/azure-function/python/kundali_lib/vedic/kalachakra.py +226 -0
  68. package/azure-function/python/kundali_lib/vedic/kartari.py +243 -0
  69. package/azure-function/python/kundali_lib/vedic/kurmachakra.py +383 -0
  70. package/azure-function/python/kundali_lib/vedic/lunar_return.py +402 -0
  71. package/azure-function/python/kundali_lib/vedic/muhurta.py +414 -0
  72. package/azure-function/python/kundali_lib/vedic/nabhasha.py +349 -0
  73. package/azure-function/python/kundali_lib/vedic/nakshatra_details.py +945 -0
  74. package/azure-function/python/kundali_lib/vedic/panchanga.py +297 -0
  75. package/azure-function/python/kundali_lib/vedic/planets.py +55 -0
  76. package/azure-function/python/kundali_lib/vedic/shadbala.py +500 -0
  77. package/azure-function/python/kundali_lib/vedic/special_conditions.py +319 -0
  78. package/azure-function/python/kundali_lib/vedic/sudarshana.py +232 -0
  79. package/azure-function/python/kundali_lib/vedic/tajaka.py +482 -0
  80. package/azure-function/python/kundali_lib/vedic/upagrahas.py +229 -0
  81. package/azure-function/python/kundali_lib/vedic/varshaphal.py +185 -0
  82. package/azure-function/python/kundali_lib/vedic/yogas.py +935 -0
  83. package/azure-function/python/kundali_lib/vedic/zodiac.py +42 -0
  84. package/azure-function/python/kundali_lib/vedicastro_bridge.py +198 -0
  85. package/azure-function/requirements.txt +9 -0
  86. package/index.js +747 -0
  87. package/kundali-chart-mcp.js +159 -0
  88. package/kundali_bridge.py +952 -0
  89. package/package.json +41 -0
  90. package/python/kundali_lib/__init__.py +1 -0
  91. package/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  92. package/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
  93. package/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
  94. package/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
  95. package/python/kundali_lib/ephemeris.py +30 -0
  96. package/python/kundali_lib/geocoder.py +82 -0
  97. package/python/kundali_lib/vedic/__init__.py +1 -0
  98. package/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
  100. package/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
  101. package/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
  102. package/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
  103. package/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
  104. package/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
  105. package/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
  106. package/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
  107. package/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
  108. package/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
  109. package/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
  110. package/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
  111. package/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
  112. package/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
  113. package/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
  114. package/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
  115. package/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
  116. package/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
  117. package/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
  118. package/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
  119. package/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
  120. package/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
  121. package/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
  122. package/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
  123. package/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
  124. package/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
  125. package/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
  126. package/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
  127. package/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
  128. package/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
  129. package/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
  130. package/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
  131. package/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
  132. package/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
  133. package/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
  134. package/python/kundali_lib/vedic/arishta.py +465 -0
  135. package/python/kundali_lib/vedic/ashtakavarga.py +213 -0
  136. package/python/kundali_lib/vedic/avasthas.py +292 -0
  137. package/python/kundali_lib/vedic/ayanamsa.py +106 -0
  138. package/python/kundali_lib/vedic/bhava_chalit.py +137 -0
  139. package/python/kundali_lib/vedic/char_dasha.py +308 -0
  140. package/python/kundali_lib/vedic/chart.py +126 -0
  141. package/python/kundali_lib/vedic/chart_types.py +338 -0
  142. package/python/kundali_lib/vedic/compatibility.py +705 -0
  143. package/python/kundali_lib/vedic/constants.py +108 -0
  144. package/python/kundali_lib/vedic/dasha_extended.py +262 -0
  145. package/python/kundali_lib/vedic/dasha_systems.py +439 -0
  146. package/python/kundali_lib/vedic/doshas.py +453 -0
  147. package/python/kundali_lib/vedic/gandanta.py +213 -0
  148. package/python/kundali_lib/vedic/gochara.py +277 -0
  149. package/python/kundali_lib/vedic/hora.py +263 -0
  150. package/python/kundali_lib/vedic/houses.py +30 -0
  151. package/python/kundali_lib/vedic/jaimini.py +361 -0
  152. package/python/kundali_lib/vedic/kalachakra.py +226 -0
  153. package/python/kundali_lib/vedic/kartari.py +243 -0
  154. package/python/kundali_lib/vedic/kurmachakra.py +383 -0
  155. package/python/kundali_lib/vedic/lunar_return.py +402 -0
  156. package/python/kundali_lib/vedic/muhurta.py +414 -0
  157. package/python/kundali_lib/vedic/nabhasha.py +349 -0
  158. package/python/kundali_lib/vedic/nakshatra_details.py +945 -0
  159. package/python/kundali_lib/vedic/panchanga.py +297 -0
  160. package/python/kundali_lib/vedic/planets.py +55 -0
  161. package/python/kundali_lib/vedic/shadbala.py +500 -0
  162. package/python/kundali_lib/vedic/special_conditions.py +319 -0
  163. package/python/kundali_lib/vedic/sudarshana.py +232 -0
  164. package/python/kundali_lib/vedic/tajaka.py +482 -0
  165. package/python/kundali_lib/vedic/upagrahas.py +229 -0
  166. package/python/kundali_lib/vedic/varshaphal.py +185 -0
  167. package/python/kundali_lib/vedic/yogas.py +935 -0
  168. package/python/kundali_lib/vedic/zodiac.py +42 -0
  169. package/python/kundali_lib/vedicastro_bridge.py +198 -0
  170. package/remote-server.js +590 -0
  171. package/requirements.txt +8 -0
  172. package/setup.sh +218 -0
@@ -0,0 +1,952 @@
1
+ """Python bridge used by the Node MCP server to call the bundled Kundali library.
2
+
3
+ This is not an MCP server. It only receives one JSON payload on stdin, calls the
4
+ standalone chart/geocoder services bundled in this package, and prints JSON on stdout.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import certifi
17
+
18
+ PACKAGE_DIR = Path(__file__).resolve().parent
19
+ PYTHON_DIR = PACKAGE_DIR / "python"
20
+ if str(PYTHON_DIR) not in sys.path:
21
+ sys.path.insert(0, str(PYTHON_DIR))
22
+
23
+ os.environ.setdefault("SSL_CERT_FILE", certifi.where())
24
+ os.environ.setdefault("REQUESTS_CA_BUNDLE", certifi.where())
25
+
26
+ from kundali_lib.ephemeris import EphemerisService # noqa: E402
27
+ from kundali_lib.geocoder import GeocoderService # noqa: E402
28
+ from kundali_lib.vedic.arishta import get_arishta_yogas # noqa: E402
29
+ from kundali_lib.vedic.ashtakavarga import get_ashtakavarga # noqa: E402
30
+ from kundali_lib.vedic.avasthas import get_avasthas # noqa: E402
31
+ from kundali_lib.vedic.ayanamsa import ( # noqa: E402
32
+ list_ayanamsa_modes,
33
+ list_house_systems,
34
+ )
35
+ from kundali_lib.vedic.bhava_chalit import get_bhava_chalit # noqa: E402
36
+ from kundali_lib.vedic.char_dasha import ( # noqa: E402
37
+ get_char_dasha,
38
+ get_narayana_dasha,
39
+ )
40
+ from kundali_lib.vedic.chart import (
41
+ get_transit_positions as calculate_transit_positions, # noqa: E402
42
+ )
43
+ from kundali_lib.vedic.chart_types import ( # noqa: E402
44
+ available_chart_types,
45
+ derive_chart,
46
+ )
47
+ from kundali_lib.vedic.compatibility import get_ashtakoot_milan # noqa: E402
48
+ from kundali_lib.vedic.dasha_extended import ( # noqa: E402
49
+ get_pratyantar_dasha,
50
+ get_yogini_dasha,
51
+ )
52
+ from kundali_lib.vedic.dasha_systems import ( # noqa: E402
53
+ get_ashtottari_dasha,
54
+ get_prana_dasha,
55
+ get_sookshma_dasha,
56
+ )
57
+ from kundali_lib.vedic.doshas import ( # noqa: E402
58
+ check_combustion,
59
+ check_graha_yuddha,
60
+ check_kaal_sarp_dosha,
61
+ check_mangal_dosha,
62
+ check_sade_sati,
63
+ )
64
+ from kundali_lib.vedic.gandanta import check_gandanta, get_gandanta_info # noqa: E402
65
+ from kundali_lib.vedic.gochara import get_gochara # noqa: E402
66
+ from kundali_lib.vedic.hora import get_hora_chart, get_planetary_hour # noqa: E402
67
+ from kundali_lib.vedic.jaimini import ( # noqa: E402
68
+ get_jaimini_aspects,
69
+ get_jaimini_karakas,
70
+ get_karakamsha,
71
+ )
72
+ from kundali_lib.vedic.kalachakra import get_kalachakra_dasha # noqa: E402
73
+ from kundali_lib.vedic.kartari import check_kartari_yoga # noqa: E402
74
+ from kundali_lib.vedic.kurmachakra import ( # noqa: E402
75
+ get_full_kurmachakra_chart,
76
+ get_kurmachakra,
77
+ get_travel_direction_score,
78
+ )
79
+ from kundali_lib.vedic.lunar_return import ( # noqa: E402
80
+ get_lunar_return,
81
+ get_prasna_chart,
82
+ )
83
+ from kundali_lib.vedic.muhurta import ( # noqa: E402
84
+ get_best_muhurta_in_range,
85
+ get_muhurta_score,
86
+ )
87
+ from kundali_lib.vedic.nabhasha import get_nabhasha_yogas # noqa: E402
88
+ from kundali_lib.vedic.nakshatra_details import ( # noqa: E402
89
+ get_nakshatra_compatibility,
90
+ get_nakshatra_details,
91
+ get_planet_nakshatra_analysis,
92
+ )
93
+ from kundali_lib.vedic.panchanga import get_panchanga # noqa: E402
94
+ from kundali_lib.vedic.shadbala import get_shadbala # noqa: E402
95
+ from kundali_lib.vedic.special_conditions import ( # noqa: E402
96
+ check_pushkara_navamsha,
97
+ check_vargottama,
98
+ get_dig_bala,
99
+ )
100
+ from kundali_lib.vedic.special_conditions import (
101
+ get_jaimini_karakas as sc_jaimini_karakas,
102
+ )
103
+ from kundali_lib.vedic.sudarshana import get_sudarshana_chakra # noqa: E402
104
+ from kundali_lib.vedic.tajaka import get_tajaka_analysis # noqa: E402
105
+ from kundali_lib.vedic.upagrahas import get_upagrahas # noqa: E402
106
+ from kundali_lib.vedic.varshaphal import get_varshaphal # noqa: E402 # noqa: E402
107
+ from kundali_lib.vedic.yogas import get_yogas # noqa: E402
108
+ from kundali_lib.vedicastro_bridge import ( # noqa: E402
109
+ get_full_chart as vc_full_chart,
110
+ )
111
+ from kundali_lib.vedicastro_bridge import (
112
+ get_planetary_aspects as vc_aspects,
113
+ )
114
+ from kundali_lib.vedicastro_bridge import (
115
+ get_significators as vc_significators,
116
+ )
117
+ from kundali_lib.vedicastro_bridge import (
118
+ get_vimshottari_dasha as vc_dasha,
119
+ )
120
+ from kundali_lib.vedicastro_bridge import (
121
+ list_vedicastro_options as vc_options,
122
+ )
123
+
124
+ ephemeris_service = EphemerisService()
125
+ geocoder_service = GeocoderService()
126
+
127
+
128
+ def parse_birth_datetime(date_of_birth: str, time_of_birth: str) -> datetime:
129
+ date_text = date_of_birth.strip()
130
+ time_text = time_of_birth.strip()
131
+
132
+ try:
133
+ if "T" in date_text:
134
+ date_part = datetime.fromisoformat(date_text).date()
135
+ else:
136
+ date_part = datetime.strptime(date_text, "%Y-%m-%d").date()
137
+ except ValueError as exc:
138
+ raise ValueError("date_of_birth must be in YYYY-MM-DD format") from exc
139
+
140
+ for fmt in ("%H:%M", "%H:%M:%S"):
141
+ try:
142
+ time_part = datetime.strptime(time_text, fmt).time()
143
+ return datetime.combine(date_part, time_part)
144
+ except ValueError:
145
+ continue
146
+
147
+ raise ValueError("time_of_birth must be in HH:MM or HH:MM:SS format")
148
+
149
+
150
+ def resolve_location(
151
+ city: str,
152
+ latitude: float | None,
153
+ longitude: float | None,
154
+ timezone: str | None,
155
+ ) -> tuple[float, float, str]:
156
+ if (latitude is None) != (longitude is None):
157
+ raise ValueError("latitude and longitude must be provided together")
158
+
159
+ if latitude is not None and longitude is not None:
160
+ resolved_timezone = (
161
+ timezone or geocoder_service.get_timezone(latitude, longitude) or "UTC"
162
+ )
163
+ return latitude, longitude, resolved_timezone
164
+
165
+ city_info = geocoder_service.get_city_info(city)
166
+ if not city_info:
167
+ raise ValueError(
168
+ f"Could not find coordinates for city '{city}'. "
169
+ "Provide latitude, longitude, and optionally timezone to avoid geocoding."
170
+ )
171
+
172
+ return (
173
+ float(city_info["latitude"]),
174
+ float(city_info["longitude"]),
175
+ str(city_info.get("timezone") or "UTC"),
176
+ )
177
+
178
+
179
+ def generate_kundali(payload: dict[str, Any]) -> dict[str, Any]:
180
+ birth_datetime = parse_birth_datetime(
181
+ payload["date_of_birth"], payload["time_of_birth"]
182
+ )
183
+ latitude, longitude, timezone = resolve_location(
184
+ city=payload["city"],
185
+ latitude=payload.get("latitude"),
186
+ longitude=payload.get("longitude"),
187
+ timezone=payload.get("timezone"),
188
+ )
189
+
190
+ base_chart = ephemeris_service.calculate_chart(
191
+ name=payload["name"],
192
+ birth_datetime=birth_datetime,
193
+ latitude=latitude,
194
+ longitude=longitude,
195
+ timezone=timezone,
196
+ ayanamsa_mode=payload.get("ayanamsa_mode"),
197
+ house_system=payload.get("house_system"),
198
+ )
199
+ return derive_chart(base_chart, payload.get("chart_type"))
200
+
201
+
202
+ def list_available_chart_types(payload: dict[str, Any]) -> dict[str, Any]:
203
+ return available_chart_types()
204
+
205
+
206
+ def list_ayanamsa_modes_tool(payload: dict[str, Any]) -> dict[str, Any]:
207
+ return list_ayanamsa_modes()
208
+
209
+
210
+ def list_house_systems_tool(payload: dict[str, Any]) -> dict[str, Any]:
211
+ return list_house_systems()
212
+
213
+
214
+ def search_birth_places(payload: dict[str, Any]) -> list[dict[str, Any]]:
215
+ limit = int(payload.get("limit") or 8)
216
+ safe_limit = max(1, min(limit, 10))
217
+ return geocoder_service.search_cities(payload["query"], limit=safe_limit)
218
+
219
+
220
+ def get_timezone_for_coordinates(payload: dict[str, Any]) -> dict[str, Any]:
221
+ latitude = float(payload["latitude"])
222
+ longitude = float(payload["longitude"])
223
+ timezone_name = geocoder_service.get_timezone(latitude, longitude) or "UTC"
224
+ return {
225
+ "latitude": latitude,
226
+ "longitude": longitude,
227
+ "timezone": timezone_name,
228
+ }
229
+
230
+
231
+ def parse_transit_datetime(payload: dict[str, Any]) -> datetime:
232
+ dt_text = (payload.get("datetime_utc") or "").strip()
233
+ if not dt_text:
234
+ return datetime.now(timezone.utc)
235
+
236
+ try:
237
+ normalized = dt_text.replace("Z", "+00:00")
238
+ parsed = datetime.fromisoformat(normalized)
239
+ except ValueError as exc:
240
+ raise ValueError(
241
+ "datetime_utc must be an ISO datetime, for example 2026-06-20T12:00:00Z"
242
+ ) from exc
243
+
244
+ if parsed.tzinfo is None:
245
+ return parsed.replace(tzinfo=timezone.utc)
246
+ return parsed.astimezone(timezone.utc)
247
+
248
+
249
+ def get_transit_positions(payload: dict[str, Any]) -> dict[str, Any]:
250
+ dt_utc = parse_transit_datetime(payload)
251
+ return {
252
+ "datetime_utc": dt_utc.isoformat(),
253
+ "planetary_positions": calculate_transit_positions(dt_utc),
254
+ }
255
+
256
+
257
+ def get_extended_chart(payload: dict[str, Any]) -> dict[str, Any]:
258
+ """Enhanced chart using vedicastro — includes SubLord/KP data."""
259
+ return vc_full_chart(payload)
260
+
261
+
262
+ def get_vimshottari_dasha(payload: dict[str, Any]) -> dict[str, Any]:
263
+ """Vimshottari Dasha (mahadasha + antardasha) for a birth chart."""
264
+ return vc_dasha(payload)
265
+
266
+
267
+ def get_planetary_aspects_tool(payload: dict[str, Any]) -> dict[str, Any]:
268
+ """Planetary aspects (conjunction, sextile, square, trine, opposition, etc.)."""
269
+ return vc_aspects(payload)
270
+
271
+
272
+ def get_significators(payload: dict[str, Any]) -> dict[str, Any]:
273
+ """Planet-wise and house-wise significators (KP system)."""
274
+ return vc_significators(payload)
275
+
276
+
277
+ def list_extended_options(payload: dict[str, Any]) -> dict[str, Any]:
278
+ """List supported ayanamsas and house systems for extended (vedicastro) tools."""
279
+ return vc_options(payload)
280
+
281
+
282
+ # ─── New comprehensive tools ───────────────────────────────────────────────
283
+
284
+
285
+ def _require_base_chart(payload: dict[str, Any]) -> dict[str, Any]:
286
+ """Generate a base chart from payload (same flow as generate_kundali, lagna type only)."""
287
+ birth_datetime = parse_birth_datetime(
288
+ payload["date_of_birth"], payload["time_of_birth"]
289
+ )
290
+ lat, lon, tz = resolve_location(
291
+ city=payload.get("city", ""),
292
+ latitude=payload.get("latitude"),
293
+ longitude=payload.get("longitude"),
294
+ timezone=payload.get("timezone"),
295
+ )
296
+ return ephemeris_service.calculate_chart(
297
+ name=payload.get("name", "Chart"),
298
+ birth_datetime=birth_datetime,
299
+ latitude=lat,
300
+ longitude=lon,
301
+ timezone=tz,
302
+ ayanamsa_mode=payload.get("ayanamsa_mode"),
303
+ house_system=payload.get("house_system"),
304
+ )
305
+
306
+
307
+ def get_panchanga_tool(payload: dict[str, Any]) -> dict[str, Any]:
308
+ """Get Panchanga (Tithi, Vara, Nakshatra, Yoga, Karana) for a given datetime and location."""
309
+ import pytz
310
+ import swisseph as swe
311
+
312
+ # Parse datetime
313
+ dt_text = (payload.get("datetime") or "").strip()
314
+ tz_name = payload.get("timezone") or "UTC"
315
+ if dt_text:
316
+ dt = datetime.fromisoformat(dt_text.replace("Z", "+00:00"))
317
+ if dt.tzinfo is None:
318
+ dt = pytz.timezone(tz_name).localize(dt)
319
+ else:
320
+ dt = datetime.now(pytz.UTC)
321
+ # Convert to JD
322
+ utc_dt = dt.astimezone(pytz.UTC)
323
+ h = utc_dt.hour + utc_dt.minute / 60.0 + utc_dt.second / 3600.0
324
+ swe.set_ephe_path(None)
325
+ swe.set_sid_mode(swe.SIDM_LAHIRI)
326
+ from kundali_lib.vedic.ayanamsa import _SIDM_MAP
327
+
328
+ ayanamsa_key = (payload.get("ayanamsa_mode") or "lahiri").lower()
329
+ swe.set_sid_mode(_SIDM_MAP.get(ayanamsa_key, swe.SIDM_LAHIRI))
330
+ jd = swe.julday(utc_dt.year, utc_dt.month, utc_dt.day, h)
331
+ flags = swe.FLG_SWIEPH | swe.FLG_SPEED | swe.FLG_SIDEREAL
332
+ sun_xx, _ = swe.calc_ut(jd, swe.SUN, flags)
333
+ moon_xx, _ = swe.calc_ut(jd, swe.MOON, flags)
334
+ panchanga = get_panchanga(sun_xx[0], moon_xx[0], jd)
335
+ panchanga["datetime"] = utc_dt.isoformat()
336
+ panchanga["ayanamsa_mode"] = ayanamsa_key
337
+ return panchanga
338
+
339
+
340
+ def get_doshas_tool(payload: dict[str, Any]) -> dict[str, Any]:
341
+ """Check all major doshas for a birth chart: Mangal, Kaal Sarp, Sade Sati, Graha Yuddha, Combustion."""
342
+ base = _require_base_chart(payload)
343
+ planets = base["planetary_positions"]
344
+ asc = base["ascendant"]
345
+ moon_rashi = base["moon_sign"]
346
+ current_saturn_rashi = payload.get("current_saturn_rashi")
347
+ result = {
348
+ "mangal_dosha": check_mangal_dosha(planets, asc),
349
+ "kaal_sarp_dosha": check_kaal_sarp_dosha(planets),
350
+ "graha_yuddha": check_graha_yuddha(planets),
351
+ "combustion": check_combustion(planets),
352
+ }
353
+ if current_saturn_rashi:
354
+ result["sade_sati"] = check_sade_sati(moon_rashi, current_saturn_rashi)
355
+ else:
356
+ result["sade_sati"] = {
357
+ "note": "Provide current_saturn_rashi to check Sade Sati"
358
+ }
359
+ return result
360
+
361
+
362
+ def get_yogas_tool(payload: dict[str, Any]) -> dict[str, Any]:
363
+ """Identify all major Vedic Yogas in a birth chart."""
364
+ base = _require_base_chart(payload)
365
+ return get_yogas(base)
366
+
367
+
368
+ def get_compatibility_tool(payload: dict[str, Any]) -> dict[str, Any]:
369
+ """Ashtakoot Milan: 36-point Vedic marriage compatibility between two birth charts."""
370
+ # Expect payload to have chart1 and chart2 sub-dicts with birth details
371
+ chart1_payload = payload["chart1"]
372
+ chart2_payload = payload["chart2"]
373
+ chart1 = _require_base_chart_from(chart1_payload)
374
+ chart2 = _require_base_chart_from(chart2_payload)
375
+ return get_ashtakoot_milan(chart1, chart2)
376
+
377
+
378
+ def _require_base_chart_from(p: dict[str, Any]) -> dict[str, Any]:
379
+ birth_datetime = parse_birth_datetime(p["date_of_birth"], p["time_of_birth"])
380
+ lat, lon, tz = resolve_location(
381
+ city=p.get("city", ""),
382
+ latitude=p.get("latitude"),
383
+ longitude=p.get("longitude"),
384
+ timezone=p.get("timezone"),
385
+ )
386
+ return ephemeris_service.calculate_chart(
387
+ name=p.get("name", "Chart"),
388
+ birth_datetime=birth_datetime,
389
+ latitude=lat,
390
+ longitude=lon,
391
+ timezone=tz,
392
+ )
393
+
394
+
395
+ def get_special_conditions_tool(payload: dict[str, Any]) -> dict[str, Any]:
396
+ """Check Vargottama, Pushkara Navamsha, Dig Bala, and Jaimini Karakas."""
397
+ from kundali_lib.vedic.chart_types import DIVISIONAL_CHARTS, derive_chart
398
+
399
+ base = _require_base_chart(payload)
400
+ d9 = derive_chart(base, "d9")
401
+ d9_positions = d9.get("planetary_positions", [])
402
+ return {
403
+ "vargottama": check_vargottama(base["planetary_positions"], d9_positions),
404
+ "pushkara_navamsha": check_pushkara_navamsha(base["planetary_positions"]),
405
+ "dig_bala": get_dig_bala(base["planetary_positions"]),
406
+ "jaimini_karakas": sc_jaimini_karakas(base["planetary_positions"]),
407
+ }
408
+
409
+
410
+ def get_pratyantar_dasha_tool(payload: dict[str, Any]) -> dict[str, Any]:
411
+ """Get Pratyantar Dasha (3rd level: Mahadasha → Antardasha → Pratyantar) for a birth chart."""
412
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
413
+ lat, lon, tz = resolve_location(
414
+ city=payload.get("city", ""),
415
+ latitude=payload.get("latitude"),
416
+ longitude=payload.get("longitude"),
417
+ timezone=payload.get("timezone"),
418
+ )
419
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, tz)
420
+ moon = next(p for p in base["planetary_positions"] if p["name"] == "Moon")
421
+ import pytz
422
+
423
+ tz_obj = pytz.timezone(tz)
424
+ birth_dt_aware = tz_obj.localize(birth_dt)
425
+ return get_pratyantar_dasha(birth_dt_aware, moon["longitude"], moon["nakshatra"])
426
+
427
+
428
+ def get_yogini_dasha_tool(payload: dict[str, Any]) -> dict[str, Any]:
429
+ """Get Yogini Dasha (8-yogini 36-year cycle) for a birth chart."""
430
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
431
+ lat, lon, tz = resolve_location(
432
+ city=payload.get("city", ""),
433
+ latitude=payload.get("latitude"),
434
+ longitude=payload.get("longitude"),
435
+ timezone=payload.get("timezone"),
436
+ )
437
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, tz)
438
+ moon = next(p for p in base["planetary_positions"] if p["name"] == "Moon")
439
+ import pytz
440
+
441
+ tz_obj = pytz.timezone(tz)
442
+ birth_dt_aware = tz_obj.localize(birth_dt)
443
+ return get_yogini_dasha(birth_dt_aware, moon["longitude"], moon["nakshatra"])
444
+
445
+
446
+ def get_varshaphal_tool(payload: dict[str, Any]) -> dict[str, Any]:
447
+ """Get Varshaphal (Solar Return chart) for a given year of life."""
448
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
449
+ lat, lon, birth_tz = resolve_location(
450
+ city=payload.get("city", ""),
451
+ latitude=payload.get("latitude"),
452
+ longitude=payload.get("longitude"),
453
+ timezone=payload.get("timezone"),
454
+ )
455
+ year_of_life = int(payload["year_of_life"])
456
+ query_lat = float(payload.get("query_latitude") or lat)
457
+ query_lon = float(payload.get("query_longitude") or lon)
458
+ query_tz = payload.get("query_timezone") or birth_tz
459
+ # get_varshaphal expects naive birth_dt and handles tz internally
460
+ return get_varshaphal(
461
+ birth_dt, lat, lon, birth_tz, year_of_life, query_lat, query_lon, query_tz
462
+ )
463
+
464
+
465
+ def get_ashtakavarga_tool(payload: dict[str, Any]) -> dict[str, Any]:
466
+ """Get Ashtakavarga (benefic point system) for a birth chart."""
467
+ base = _require_base_chart(payload)
468
+ return get_ashtakavarga(base)
469
+
470
+
471
+ def get_shadbala_tool(payload: dict[str, Any]) -> dict[str, Any]:
472
+ """Get Shadbala (six-fold planetary strength) for a birth chart."""
473
+ base = _require_base_chart(payload)
474
+ return get_shadbala(base)
475
+
476
+
477
+ def get_jaimini_tool(payload: dict[str, Any]) -> dict[str, Any]:
478
+ """Get full Jaimini astrology analysis: Karakas, aspects, and Karakamsha."""
479
+ from kundali_lib.vedic.chart_types import derive_chart
480
+
481
+ base = _require_base_chart(payload)
482
+ d9 = derive_chart(base, "d9")
483
+ d9_positions = d9.get("planetary_positions", [])
484
+ return {
485
+ "karakas": get_jaimini_karakas(base["planetary_positions"]),
486
+ "aspects": get_jaimini_aspects(base),
487
+ "karakamsha": get_karakamsha(base["planetary_positions"], d9_positions),
488
+ }
489
+
490
+
491
+ def get_muhurta_tool(payload: dict[str, Any]) -> dict[str, Any]:
492
+ """Score a datetime for Muhurta (auspicious timing) for a given event type."""
493
+ panchanga = get_panchanga_tool(payload)
494
+ # Get planetary positions for the same moment (for aspects context)
495
+ import pytz
496
+ import swisseph as swe
497
+
498
+ swe.set_ephe_path(None)
499
+ swe.set_sid_mode(swe.SIDM_LAHIRI)
500
+ dt_text = (payload.get("datetime") or "").strip()
501
+ tz_name = payload.get("timezone") or "UTC"
502
+ if dt_text:
503
+ dt = datetime.fromisoformat(dt_text.replace("Z", "+00:00"))
504
+ if dt.tzinfo is None:
505
+ dt = pytz.timezone(tz_name).localize(dt)
506
+ else:
507
+ dt = datetime.now(pytz.UTC)
508
+ event_type = payload.get("event_type") or "general"
509
+ return get_muhurta_score(panchanga, [], event_type)
510
+
511
+
512
+ def get_best_muhurta_tool(payload: dict[str, Any]) -> dict[str, Any]:
513
+ """Find the best Muhurta windows in a date range for an event type."""
514
+ from datetime import datetime as _dt
515
+
516
+ import pytz
517
+
518
+ start_str = payload["start_datetime"]
519
+ end_str = payload["end_datetime"]
520
+ tz_name = payload.get("timezone") or "UTC"
521
+ lat = float(payload.get("latitude") or 28.6)
522
+ lon = float(payload.get("longitude") or 77.2)
523
+ event_type = payload.get("event_type") or "general"
524
+ tz_obj = pytz.timezone(tz_name)
525
+ start_dt = tz_obj.localize(_dt.fromisoformat(start_str))
526
+ end_dt = tz_obj.localize(_dt.fromisoformat(end_str))
527
+ return {
528
+ "best_windows": get_best_muhurta_in_range(
529
+ start_dt, end_dt, lat, lon, event_type
530
+ )
531
+ }
532
+
533
+
534
+ # ─── New comprehensive tools batch 2 ──────────────────────────────────────────
535
+
536
+
537
+ def get_avasthas_tool(payload: dict[str, Any]) -> dict[str, Any]:
538
+ """Get planetary Avasthas (9 states) and Baladi Avasthas for a birth chart."""
539
+ base = _require_base_chart(payload)
540
+ return {
541
+ "avasthas": get_avasthas(base["planetary_positions"], base["ascendant"]),
542
+ "ayanamsa": base.get("ayanamsa_name"),
543
+ }
544
+
545
+
546
+ def get_upagrahas_tool(payload: dict[str, Any]) -> dict[str, Any]:
547
+ """Get Upagrahas: Gulika, Mandi, Dhuma, Vyatipata, Parivesha, Indra Chapa, Upaketu."""
548
+ import pytz
549
+ import swisseph as swe
550
+
551
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
552
+ lat, lon, tz = resolve_location(
553
+ city=payload.get("city", ""),
554
+ latitude=payload.get("latitude"),
555
+ longitude=payload.get("longitude"),
556
+ timezone=payload.get("timezone"),
557
+ )
558
+ swe.set_ephe_path(None)
559
+ swe.set_sid_mode(swe.SIDM_LAHIRI)
560
+ tz_obj = pytz.timezone(tz)
561
+ birth_aware = tz_obj.localize(birth_dt)
562
+ utc = birth_aware.astimezone(pytz.UTC)
563
+ h = utc.hour + utc.minute / 60.0 + utc.second / 3600.0
564
+ jd = swe.julday(utc.year, utc.month, utc.day, h)
565
+ ayanamsa = swe.get_ayanamsa_ut(jd)
566
+ return get_upagrahas(jd, lat, lon, ayanamsa)
567
+
568
+
569
+ def get_gandanta_tool(payload: dict[str, Any]) -> dict[str, Any]:
570
+ """Check which planets (if any) are in Gandanta zones (water-fire sign junctions)."""
571
+ base = _require_base_chart(payload)
572
+ return {
573
+ "gandanta_planets": check_gandanta(base["planetary_positions"]),
574
+ "gandanta_zones_info": get_gandanta_info(),
575
+ }
576
+
577
+
578
+ def get_kartari_tool(payload: dict[str, Any]) -> dict[str, Any]:
579
+ """Check Kartari Yoga: planets or houses hemmed between benefics (Subha) or malefics (Papa)."""
580
+ base = _require_base_chart(payload)
581
+ # Convert houses dict from {"House_1": "Capricorn", ...} to {1: ["Sun", ...], ...}
582
+ houses_map: dict[int, list[str]] = {}
583
+ for planet in base["planetary_positions"]:
584
+ h = int(planet["house"])
585
+ houses_map.setdefault(h, []).append(planet["name"])
586
+ return {
587
+ "kartari_yogas": check_kartari_yoga(base["planetary_positions"], houses_map)
588
+ }
589
+
590
+
591
+ def get_nakshatra_tool(payload: dict[str, Any]) -> dict[str, Any]:
592
+ """Get deep Nakshatra analysis: deity, symbol, gana, element, padas, and keywords for each planet."""
593
+ base = _require_base_chart(payload)
594
+ result = {
595
+ "planet_nakshatra_analysis": get_planet_nakshatra_analysis(
596
+ base["planetary_positions"]
597
+ )
598
+ }
599
+ if payload.get("nakshatra_name"):
600
+ result["nakshatra_details"] = get_nakshatra_details(payload["nakshatra_name"])
601
+ return result
602
+
603
+
604
+ def get_gochara_tool(payload: dict[str, Any]) -> dict[str, Any]:
605
+ """Get Gochara (Vedic transit analysis): how current transiting planets affect natal Moon houses."""
606
+ natal = _require_base_chart(payload)
607
+ # Build transit chart for current or specified datetime
608
+ import pytz
609
+ import swisseph as swe
610
+
611
+ dt_text = (payload.get("transit_datetime") or "").strip()
612
+ swe.set_ephe_path(None)
613
+ swe.set_sid_mode(swe.SIDM_LAHIRI)
614
+ if dt_text:
615
+ dt = datetime.fromisoformat(dt_text.replace("Z", "+00:00"))
616
+ if dt.tzinfo is None:
617
+ dt = pytz.UTC.localize(dt)
618
+ else:
619
+ dt = datetime.now(pytz.UTC)
620
+ utc = dt.astimezone(pytz.UTC)
621
+ h = utc.hour + utc.minute / 60.0 + utc.second / 3600.0
622
+ jd = swe.julday(utc.year, utc.month, utc.day, h)
623
+ flags = swe.FLG_SWIEPH | swe.FLG_SPEED | swe.FLG_SIDEREAL
624
+ from kundali_lib.vedic.chart import get_transit_positions as _gtp
625
+
626
+ transit_planets = _gtp(utc)
627
+ return get_gochara(natal, transit_planets)
628
+
629
+
630
+ def get_bhava_chalit_tool(payload: dict[str, Any]) -> dict[str, Any]:
631
+ """Get Bhava Chalit chart: house positions based on equal divisions from house cusps."""
632
+ base = _require_base_chart(payload)
633
+ return get_bhava_chalit(base)
634
+
635
+
636
+ def get_hora_tool(payload: dict[str, Any]) -> dict[str, Any]:
637
+ """Get Planetary Hour (Hora) for a given datetime and location."""
638
+ import pytz
639
+
640
+ dt_text = (payload.get("datetime") or "").strip()
641
+ tz_name = payload.get("timezone") or "UTC"
642
+ lat = float(payload.get("latitude") or 28.6)
643
+ lon = float(payload.get("longitude") or 77.2)
644
+ if dt_text:
645
+ dt = datetime.fromisoformat(dt_text.replace("Z", "+00:00"))
646
+ if dt.tzinfo is None:
647
+ dt = pytz.timezone(tz_name).localize(dt)
648
+ dt_utc = dt.astimezone(pytz.UTC)
649
+ else:
650
+ dt_utc = datetime.now(pytz.UTC)
651
+ return get_planetary_hour(dt_utc, lat, lon)
652
+
653
+
654
+ def get_sudarshana_tool(payload: dict[str, Any]) -> dict[str, Any]:
655
+ """Get Sudarshana Chakra: triple-layer analysis from Lagna, Moon, and Sun simultaneously."""
656
+ base = _require_base_chart(payload)
657
+ return get_sudarshana_chakra(base)
658
+
659
+
660
+ def get_ashtottari_tool(payload: dict[str, Any]) -> dict[str, Any]:
661
+ """Get Ashtottari Dasha (108-year cycle alternative to Vimshottari)."""
662
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
663
+ lat, lon, tz = resolve_location(
664
+ city=payload.get("city", ""),
665
+ latitude=payload.get("latitude"),
666
+ longitude=payload.get("longitude"),
667
+ timezone=payload.get("timezone"),
668
+ )
669
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, tz)
670
+ moon = next(p for p in base["planetary_positions"] if p["name"] == "Moon")
671
+ import pytz
672
+
673
+ birth_aware = pytz.timezone(tz).localize(birth_dt)
674
+ return get_ashtottari_dasha(birth_aware, moon["longitude"], moon["nakshatra"])
675
+
676
+
677
+ def get_sookshma_tool(payload: dict[str, Any]) -> dict[str, Any]:
678
+ """Get Sookshma Dasha (4th level: MD→AD→PD→SD) — shows current running period at all 4 levels."""
679
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
680
+ lat, lon, tz = resolve_location(
681
+ city=payload.get("city", ""),
682
+ latitude=payload.get("latitude"),
683
+ longitude=payload.get("longitude"),
684
+ timezone=payload.get("timezone"),
685
+ )
686
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, tz)
687
+ moon = next(p for p in base["planetary_positions"] if p["name"] == "Moon")
688
+ # get_sookshma_dasha expects naive birth_dt
689
+ return get_sookshma_dasha(birth_dt, moon["longitude"], moon["nakshatra"])
690
+
691
+
692
+ def get_prana_tool(payload: dict[str, Any]) -> dict[str, Any]:
693
+ """Get Prana Dasha (5th level: MD→AD→PD→SD→PRD) — finest level of Vimshottari Dasha."""
694
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
695
+ lat, lon, tz = resolve_location(
696
+ city=payload.get("city", ""),
697
+ latitude=payload.get("latitude"),
698
+ longitude=payload.get("longitude"),
699
+ timezone=payload.get("timezone"),
700
+ )
701
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, tz)
702
+ moon = next(p for p in base["planetary_positions"] if p["name"] == "Moon")
703
+ # get_prana_dasha expects naive birth_dt
704
+ return get_prana_dasha(birth_dt, moon["longitude"], moon["nakshatra"])
705
+
706
+
707
+ def get_char_dasha_tool(payload: dict[str, Any]) -> dict[str, Any]:
708
+ """Get Char Dasha (Jaimini sign-based dasha system)."""
709
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
710
+ lat, lon, tz = resolve_location(
711
+ city=payload.get("city", ""),
712
+ latitude=payload.get("latitude"),
713
+ longitude=payload.get("longitude"),
714
+ timezone=payload.get("timezone"),
715
+ )
716
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, tz)
717
+ import pytz
718
+
719
+ birth_aware = pytz.timezone(tz).localize(birth_dt)
720
+ return get_char_dasha(base, birth_aware)
721
+
722
+
723
+ def get_narayana_tool(payload: dict[str, Any]) -> dict[str, Any]:
724
+ """Get Narayana Dasha (Jaimini sign-based dasha for worldly events)."""
725
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
726
+ lat, lon, tz = resolve_location(
727
+ city=payload.get("city", ""),
728
+ latitude=payload.get("latitude"),
729
+ longitude=payload.get("longitude"),
730
+ timezone=payload.get("timezone"),
731
+ )
732
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, tz)
733
+ import pytz
734
+
735
+ birth_aware = pytz.timezone(tz).localize(birth_dt)
736
+ return get_narayana_dasha(base, birth_aware)
737
+
738
+
739
+ def get_tajaka_tool(payload: dict[str, Any]) -> dict[str, Any]:
740
+ """Get Tajaka analysis for Varshaphal (annual chart): Muntha, Munthesh, and Tajaka Yogas."""
741
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
742
+ lat, lon, birth_tz = resolve_location(
743
+ city=payload.get("city", ""),
744
+ latitude=payload.get("latitude"),
745
+ longitude=payload.get("longitude"),
746
+ timezone=payload.get("timezone"),
747
+ )
748
+ year_of_life = int(payload["year_of_life"])
749
+ query_lat = float(payload.get("query_latitude") or lat)
750
+ query_lon = float(payload.get("query_longitude") or lon)
751
+ query_tz = payload.get("query_timezone") or birth_tz
752
+ # get_varshaphal expects naive birth_dt
753
+ birth_chart = ephemeris_service.calculate_chart(
754
+ "Chart", birth_dt, lat, lon, birth_tz
755
+ )
756
+ varsha = get_varshaphal(
757
+ birth_dt, lat, lon, birth_tz, year_of_life, query_lat, query_lon, query_tz
758
+ )
759
+ varsha_chart = varsha.get("varshaphal_chart", {})
760
+ return get_tajaka_analysis(birth_chart, varsha_chart, year_of_life)
761
+
762
+
763
+ def get_nabhasha_tool(payload: dict[str, Any]) -> dict[str, Any]:
764
+ """Get Nabhasha Yogas: special yogas based on overall planetary distribution pattern."""
765
+ base = _require_base_chart(payload)
766
+ return get_nabhasha_yogas(base["planetary_positions"])
767
+
768
+
769
+ def get_arishta_tool(payload: dict[str, Any]) -> dict[str, Any]:
770
+ """Get Arishta Yogas: longevity assessment and affliction indicators."""
771
+ base = _require_base_chart(payload)
772
+ return get_arishta_yogas(base)
773
+
774
+
775
+ def get_lunar_return_tool(payload: dict[str, Any]) -> dict[str, Any]:
776
+ """Get Lunar Return (Chandraphal): monthly chart for when Moon returns to natal position."""
777
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
778
+ lat, lon, birth_tz = resolve_location(
779
+ city=payload.get("city", ""),
780
+ latitude=payload.get("latitude"),
781
+ longitude=payload.get("longitude"),
782
+ timezone=payload.get("timezone"),
783
+ )
784
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, birth_tz)
785
+ moon = next(p for p in base["planetary_positions"] if p["name"] == "Moon")
786
+ target_year = int(payload["target_year"])
787
+ target_month = int(payload["target_month"])
788
+ query_lat = float(payload.get("query_latitude") or lat)
789
+ query_lon = float(payload.get("query_longitude") or lon)
790
+ query_tz = payload.get("query_timezone") or birth_tz
791
+ # get_lunar_return expects naive birth_dt (it localizes internally)
792
+ return get_lunar_return(
793
+ {
794
+ "moon_sign": moon["rashi"],
795
+ "planetary_positions": base["planetary_positions"],
796
+ },
797
+ birth_dt,
798
+ target_year,
799
+ target_month,
800
+ query_lat,
801
+ query_lon,
802
+ query_tz,
803
+ )
804
+
805
+
806
+ def get_prasna_tool(payload: dict[str, Any]) -> dict[str, Any]:
807
+ """Get Prasna (Horary) chart for the moment a question is asked."""
808
+ import pytz
809
+
810
+ dt_text = (payload.get("datetime") or "").strip()
811
+ tz_name = payload.get("timezone") or "UTC"
812
+ lat = float(payload.get("latitude") or 28.6)
813
+ lon = float(payload.get("longitude") or 77.2)
814
+ question = payload.get("question") or ""
815
+ if dt_text:
816
+ dt = datetime.fromisoformat(dt_text.replace("Z", "+00:00"))
817
+ if dt.tzinfo is None:
818
+ dt = pytz.timezone(tz_name).localize(dt)
819
+ dt_utc = dt.astimezone(pytz.UTC)
820
+ else:
821
+ dt_utc = datetime.now(pytz.UTC)
822
+ return get_prasna_chart(dt_utc, lat, lon, question)
823
+
824
+
825
+ def _moon_nakshatra_from_payload(payload: dict[str, Any]) -> dict[str, Any]:
826
+ """Helper: build a base chart and return Moon nakshatra + longitude + tz-aware birth_dt."""
827
+ birth_dt = parse_birth_datetime(payload["date_of_birth"], payload["time_of_birth"])
828
+ lat, lon, tz = resolve_location(
829
+ city=payload.get("city", ""),
830
+ latitude=payload.get("latitude"),
831
+ longitude=payload.get("longitude"),
832
+ timezone=payload.get("timezone"),
833
+ )
834
+ base = ephemeris_service.calculate_chart("Chart", birth_dt, lat, lon, tz)
835
+ moon = next(p for p in base["planetary_positions"] if p["name"] == "Moon")
836
+ import pytz
837
+
838
+ return {
839
+ "moon_nakshatra": moon["nakshatra"],
840
+ "moon_longitude": moon["longitude"],
841
+ "birth_aware": pytz.timezone(tz).localize(birth_dt),
842
+ }
843
+
844
+
845
+ def get_kalachakra_dasha_tool(payload: dict[str, Any]) -> dict[str, Any]:
846
+ """Get Kalachakra Dasha: 9-group 112-year sign-based cycle (one of the most complex dasha systems)."""
847
+ info = _moon_nakshatra_from_payload(payload)
848
+ return get_kalachakra_dasha(
849
+ info["birth_aware"], info["moon_longitude"], info["moon_nakshatra"]
850
+ )
851
+
852
+
853
+ def get_kurmachakra_tool(payload: dict[str, Any]) -> dict[str, Any]:
854
+ """Get Kurmachakra (Tortoise Wheel) directional analysis: best/avoid directions for travel, construction, marriage."""
855
+ info = _moon_nakshatra_from_payload(payload)
856
+ return get_kurmachakra(
857
+ birth_nakshatra=info["moon_nakshatra"],
858
+ current_moon_nakshatra=payload.get("current_moon_nakshatra"),
859
+ event_type=payload.get("event_type", "travel"),
860
+ )
861
+
862
+
863
+ def get_travel_direction_tool(payload: dict[str, Any]) -> dict[str, Any]:
864
+ """Score a specific intended travel direction for auspiciousness using Kurmachakra rules."""
865
+ info = _moon_nakshatra_from_payload(payload)
866
+ return get_travel_direction_score(
867
+ birth_nakshatra=info["moon_nakshatra"],
868
+ intended_direction=payload.get("intended_direction", "North"),
869
+ current_moon_nakshatra=payload.get("current_moon_nakshatra"),
870
+ weekday=payload.get("weekday"),
871
+ )
872
+
873
+
874
+ def get_full_kurmachakra_chart_tool(payload: dict[str, Any]) -> dict[str, Any]:
875
+ """Return the full Kurmachakra chart: all 9 directions + their nakshatras + weekday-avoid rules."""
876
+ return get_full_kurmachakra_chart()
877
+
878
+
879
+ TOOLS = {
880
+ # Core chart tools
881
+ "generate_kundali": generate_kundali,
882
+ "list_available_chart_types": list_available_chart_types,
883
+ "list_ayanamsa_modes": list_ayanamsa_modes_tool,
884
+ "list_house_systems": list_house_systems_tool,
885
+ "search_birth_places": search_birth_places,
886
+ "get_timezone_for_coordinates": get_timezone_for_coordinates,
887
+ "get_transit_positions": get_transit_positions,
888
+ # vedicastro / KP tools
889
+ "get_extended_chart": get_extended_chart,
890
+ "get_vimshottari_dasha": get_vimshottari_dasha,
891
+ "get_planetary_aspects": get_planetary_aspects_tool,
892
+ "get_significators": get_significators,
893
+ "list_extended_options": list_extended_options,
894
+ # New comprehensive tools
895
+ "get_panchanga": get_panchanga_tool,
896
+ "get_doshas": get_doshas_tool,
897
+ "get_yogas": get_yogas_tool,
898
+ "get_compatibility": get_compatibility_tool,
899
+ "get_special_conditions": get_special_conditions_tool,
900
+ "get_pratyantar_dasha": get_pratyantar_dasha_tool,
901
+ "get_yogini_dasha": get_yogini_dasha_tool,
902
+ "get_varshaphal": get_varshaphal_tool,
903
+ "get_ashtakavarga": get_ashtakavarga_tool,
904
+ "get_shadbala": get_shadbala_tool,
905
+ "get_jaimini": get_jaimini_tool,
906
+ "get_muhurta": get_muhurta_tool,
907
+ "get_best_muhurta": get_best_muhurta_tool,
908
+ # Batch 2 — new comprehensive tools
909
+ "get_avasthas": get_avasthas_tool,
910
+ "get_upagrahas": get_upagrahas_tool,
911
+ "get_gandanta": get_gandanta_tool,
912
+ "get_kartari": get_kartari_tool,
913
+ "get_nakshatra_analysis": get_nakshatra_tool,
914
+ "get_gochara": get_gochara_tool,
915
+ "get_bhava_chalit": get_bhava_chalit_tool,
916
+ "get_hora": get_hora_tool,
917
+ "get_sudarshana_chakra": get_sudarshana_tool,
918
+ "get_ashtottari_dasha": get_ashtottari_tool,
919
+ "get_sookshma_dasha": get_sookshma_tool,
920
+ "get_prana_dasha": get_prana_tool,
921
+ "get_char_dasha": get_char_dasha_tool,
922
+ "get_narayana_dasha": get_narayana_tool,
923
+ "get_tajaka": get_tajaka_tool,
924
+ "get_nabhasha_yogas": get_nabhasha_tool,
925
+ "get_arishta": get_arishta_tool,
926
+ "get_lunar_return": get_lunar_return_tool,
927
+ "get_prasna": get_prasna_tool,
928
+ # Kalachakra + Kurmachakra tools (final additions)
929
+ "get_kalachakra_dasha": get_kalachakra_dasha_tool,
930
+ "get_kurmachakra": get_kurmachakra_tool,
931
+ "get_travel_direction_score": get_travel_direction_tool,
932
+ "get_full_kurmachakra_chart": get_full_kurmachakra_chart_tool,
933
+ }
934
+
935
+
936
+ def main() -> int:
937
+ if len(sys.argv) != 2 or sys.argv[1] not in TOOLS:
938
+ print("Usage: kundali_bridge.py <tool-name>", file=sys.stderr)
939
+ return 2
940
+
941
+ try:
942
+ payload = json.loads(sys.stdin.read() or "{}")
943
+ result = TOOLS[sys.argv[1]](payload)
944
+ print(json.dumps(result, ensure_ascii=False))
945
+ return 0
946
+ except Exception as exc:
947
+ print(str(exc), file=sys.stderr)
948
+ return 1
949
+
950
+
951
+ if __name__ == "__main__":
952
+ raise SystemExit(main())