Spaces:
Running
Running
| """ | |
| MCP Client for HERMES | |
| Connects to local or remote MCP servers (Modal deployment) | |
| """ | |
| import os | |
| import json | |
| from typing import Dict, List, Optional, Any | |
| import modal | |
| # Modal app reference | |
| try: | |
| # Connect to deployed Modal app | |
| stub = modal.Stub.lookup("hermes-astrology-mcp", create_if_missing=False) | |
| MODAL_AVAILABLE = True | |
| except Exception as e: | |
| print(f"Modal app not found: {e}") | |
| MODAL_AVAILABLE = False | |
| class MCPClient: | |
| """Client for interacting with HERMES MCP servers""" | |
| def __init__(self, mode: str = "local"): | |
| """ | |
| Initialize MCP client | |
| Args: | |
| mode: "local" for local functions, "modal" for Modal serverless | |
| """ | |
| self.mode = mode | |
| if mode == "modal" and not MODAL_AVAILABLE: | |
| print("⚠️ Modal not available, falling back to local mode") | |
| self.mode = "local" | |
| def get_bound_ruler(self, sign: str, degree: float) -> Dict[str, Any]: | |
| """Get Egyptian bound (term) ruler for a position""" | |
| if self.mode == "modal": | |
| try: | |
| func = stub.get_bound_ruler | |
| return func.remote(sign, degree) | |
| except Exception as e: | |
| print(f"Modal error: {e}, falling back to local") | |
| return self._local_get_bound_ruler(sign, degree) | |
| else: | |
| return self._local_get_bound_ruler(sign, degree) | |
| def _local_get_bound_ruler(self, sign: str, degree: float) -> Dict[str, Any]: | |
| """Local implementation of bound ruler""" | |
| from app import EGYPTIAN_BOUNDS | |
| if sign not in EGYPTIAN_BOUNDS: | |
| return {"error": f"Invalid sign: {sign}"} | |
| bounds = EGYPTIAN_BOUNDS[sign] | |
| for start, end, ruler in bounds: | |
| if start <= degree < end: | |
| return { | |
| "sign": sign, | |
| "degree": degree, | |
| "bound_ruler": ruler, | |
| "bound_range": f"{start}-{end}°", | |
| "dignity_points": 2 | |
| } | |
| return {"error": "Degree out of range"} | |
| def get_decan_ruler(self, sign: str, degree: float) -> Dict[str, Any]: | |
| """Get decan (face) ruler for a position""" | |
| if self.mode == "modal": | |
| try: | |
| func = stub.get_decan_ruler | |
| return func.remote(sign, degree) | |
| except Exception as e: | |
| print(f"Modal error: {e}, falling back to local") | |
| return self._local_get_decan_ruler(sign, degree) | |
| else: | |
| return self._local_get_decan_ruler(sign, degree) | |
| def _local_get_decan_ruler(self, sign: str, degree: float) -> Dict[str, Any]: | |
| """Local implementation of decan ruler""" | |
| from app import DECANS | |
| if sign not in DECANS: | |
| return {"error": f"Invalid sign: {sign}"} | |
| decans = DECANS[sign] | |
| for start, end, ruler in decans: | |
| if start <= degree < end: | |
| return { | |
| "sign": sign, | |
| "degree": degree, | |
| "decan_ruler": ruler, | |
| "decan_number": (start // 10) + 1, | |
| "decan_range": f"{start}-{end}°", | |
| "dignity_points": 1 | |
| } | |
| return {"error": "Degree out of range"} | |
| def calculate_zodiacal_releasing( | |
| self, | |
| starting_sign: str, | |
| birth_date: str, | |
| calculation_date: Optional[str] = None | |
| ) -> Dict[str, Any]: | |
| """Calculate Zodiacal Releasing periods""" | |
| if self.mode == "modal": | |
| try: | |
| func = stub.calculate_zodiacal_releasing | |
| return func.remote(starting_sign, birth_date, calculation_date) | |
| except Exception as e: | |
| print(f"Modal error: {e}, falling back to local") | |
| return {"error": f"Local ZR not implemented: {e}"} | |
| else: | |
| return {"error": "Zodiacal Releasing only available via Modal server"} | |
| def calculate_firdaria( | |
| self, | |
| birth_date: str, | |
| is_day_chart: bool, | |
| years_to_calculate: int = 75 | |
| ) -> Dict[str, Any]: | |
| """Calculate Firdaria (Persian time-lord) periods""" | |
| if self.mode == "modal": | |
| try: | |
| func = stub.calculate_firdaria | |
| return func.remote(birth_date, is_day_chart, years_to_calculate) | |
| except Exception as e: | |
| print(f"Modal error: {e}, falling back to local") | |
| return {"error": f"Local Firdaria not implemented: {e}"} | |
| else: | |
| return {"error": "Firdaria only available via Modal server"} | |
| def calculate_full_dignities( | |
| self, | |
| planet: str, | |
| sign: str, | |
| degree: float, | |
| is_day_chart: bool | |
| ) -> Dict[str, Any]: | |
| """Calculate complete essential dignities including bounds and decans""" | |
| # Get bound | |
| bound_result = self.get_bound_ruler(sign, degree) | |
| decan_result = self.get_decan_ruler(sign, degree) | |
| # Calculate base dignities (from app.py) | |
| from app import (DOMICILE_RULERS, EXALTATIONS, DETRIMENTS, FALLS, | |
| TRIPLICITIES, SIGN_TRIPLICITIES) | |
| dignity_score = 0 | |
| dignities = {} | |
| # Domicile | |
| if DOMICILE_RULERS.get(sign) == planet: | |
| dignity_score += 5 | |
| dignities["domicile"] = {"points": 5, "ruler": planet} | |
| # Exaltation | |
| if EXALTATIONS.get(sign) == planet: | |
| dignity_score += 4 | |
| dignities["exaltation"] = {"points": 4, "ruler": planet} | |
| # Triplicity | |
| triplicity = SIGN_TRIPLICITIES.get(sign) | |
| if triplicity: | |
| trip_rulers = TRIPLICITIES[triplicity] | |
| if is_day_chart and trip_rulers["day"] == planet: | |
| dignity_score += 3 | |
| dignities["triplicity"] = {"points": 3, "type": "day", "ruler": planet} | |
| elif not is_day_chart and trip_rulers["night"] == planet: | |
| dignity_score += 3 | |
| dignities["triplicity"] = {"points": 3, "type": "night", "ruler": planet} | |
| elif trip_rulers["participating"] == planet: | |
| dignity_score += 3 | |
| dignities["triplicity"] = {"points": 3, "type": "participating", "ruler": planet} | |
| # Bound | |
| if "bound_ruler" in bound_result and bound_result["bound_ruler"] == planet: | |
| dignity_score += 2 | |
| dignities["bound"] = bound_result | |
| # Decan | |
| if "decan_ruler" in decan_result and decan_result["decan_ruler"] == planet: | |
| dignity_score += 1 | |
| dignities["decan"] = decan_result | |
| # Detriment | |
| if sign in DETRIMENTS.get(planet, []): | |
| dignity_score -= 5 | |
| dignities["detriment"] = {"points": -5, "sign": sign} | |
| # Fall | |
| if sign in FALLS.get(planet, []): | |
| dignity_score -= 4 | |
| dignities["fall"] = {"points": -4, "sign": sign} | |
| return { | |
| "planet": planet, | |
| "sign": sign, | |
| "degree": degree, | |
| "sect": "day" if is_day_chart else "night", | |
| "dignities": dignities, | |
| "total_score": dignity_score | |
| } | |
| def get_tools(self) -> List[Dict[str, Any]]: | |
| """Get list of available MCP tools""" | |
| tools = [ | |
| { | |
| "name": "get_bound_ruler", | |
| "description": "Get Egyptian bound (term) ruler for a position", | |
| "parameters": ["sign", "degree"], | |
| "available": "local+modal" | |
| }, | |
| { | |
| "name": "get_decan_ruler", | |
| "description": "Get decan (face) ruler for a position", | |
| "parameters": ["sign", "degree"], | |
| "available": "local+modal" | |
| }, | |
| { | |
| "name": "calculate_full_dignities", | |
| "description": "Calculate complete essential dignities", | |
| "parameters": ["planet", "sign", "degree", "is_day_chart"], | |
| "available": "local+modal" | |
| }, | |
| { | |
| "name": "calculate_zodiacal_releasing", | |
| "description": "Calculate Zodiacal Releasing periods", | |
| "parameters": ["starting_sign", "birth_date", "calculation_date"], | |
| "available": "modal" if self.mode == "modal" else "modal-only" | |
| }, | |
| { | |
| "name": "calculate_firdaria", | |
| "description": "Calculate Firdaria (Persian time-lord) periods", | |
| "parameters": ["birth_date", "is_day_chart", "years_to_calculate"], | |
| "available": "modal" if self.mode == "modal" else "modal-only" | |
| } | |
| ] | |
| return tools | |
| # Convenience functions | |
| def create_mcp_client(prefer_modal: bool = True) -> MCPClient: | |
| """Create an MCP client, preferring Modal if available""" | |
| if prefer_modal and MODAL_AVAILABLE: | |
| return MCPClient(mode="modal") | |
| return MCPClient(mode="local") | |