Download services/supabase_service.py from JohnAlbarkaIbrahim/SentimentScope: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/spaces/JohnAlbarkaIbrahim/SentimentScope/resolve/main/services/supabase_service.py
- Command line
-
hf download hf://spaces/JohnAlbarkaIbrahim/SentimentScope/services/supabase_service.py
-
curl -L -o supabase_service.py https://huggingface.co/spaces/JohnAlbarkaIbrahim/SentimentScope/resolve/main/services/supabase_service.py
15.2 kB
| import os | |
| import re | |
| from datetime import date as dt_date | |
| from supabase import create_client, Client | |
| from services.embedding_service import get_embedding_model | |
| from config import SUPABASE_URL, SUPABASE_KEY | |
| supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) | |
| embedder = get_embedding_model() | |
| _PYTHON_SIDE_TOOLS = {"generate_data_export", "geocode_address", "suggest_actions", "generate_contact_buttons"} | |
| _VALID_POSITIONS = {"top", "bottom", "middle"} | |
| def is_valid_uuid(val) -> bool: | |
| """Validates whether a value is a valid UUID string.""" | |
| if not isinstance(val, str): | |
| return False | |
| return bool(re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', val.lower())) | |
| def _first_val(d: dict, *keys, default=None): | |
| """Returns the first non-empty, non-None value among keys in dict d, or default.""" | |
| for k in keys: | |
| val = d.get(k) | |
| if val is not None and val != "": | |
| return val | |
| return default | |
| def _clamp(value, lo, hi, default=None): | |
| """Clamps an integer parameter to [lo, hi]. Returns default on missing or invalid value.""" | |
| if value is None: | |
| return default | |
| try: | |
| return max(lo, min(int(value), hi)) | |
| except (ValueError, TypeError): | |
| return default | |
| def _norm_rank_pos(pos) -> str: | |
| """Normalizes rank position to 'top', 'bottom', or 'middle'.""" | |
| pos_str = str(pos or "top").lower() | |
| return pos_str if pos_str in _VALID_POSITIONS else "top" | |
| def _norm_kpis(args: dict) -> dict: | |
| """Normalizes parameters for get_real_estate_kpis.""" | |
| bedrooms = _first_val(args, "p_bedrooms", "bedrooms") | |
| return { | |
| "p_market": _first_val(args, "p_market", "market"), | |
| "p_platform": _first_val(args, "p_platform", "platform"), | |
| "p_bedrooms": int(bedrooms) if bedrooms is not None else None, | |
| "p_is_active": args.get("p_is_active") if "p_is_active" in args else True, | |
| "p_property_ids": _first_val(args, "p_property_ids", "property_ids"), | |
| "p_start_date": _first_val(args, "p_start_date", "start_date"), | |
| "p_end_date": _first_val(args, "p_end_date", "end_date"), | |
| } | |
| def _norm_market_averages(args: dict) -> dict: | |
| """Normalizes parameters for get_market_averages.""" | |
| market = _first_val(args, "market_param", "p_market", "market") | |
| return {"market_param": str(market)} if market else {} | |
| def _norm_market_snapshot(args: dict) -> dict: | |
| """Normalizes parameters for get_market_snapshot with date range validation.""" | |
| market = _first_val(args, "p_market", "market") | |
| if not market: | |
| return {"status": "error", "message": "p_market is required for get_market_snapshot. Call get_tracked_markets to see available markets."} | |
| start = _first_val(args, "p_start_date", "start_date") | |
| end = _first_val(args, "p_end_date", "end_date") | |
| if not start or not end: | |
| return {"status": "error", "message": "get_market_snapshot requires both p_start_date and p_end_date."} | |
| try: | |
| d1 = dt_date.fromisoformat(start) | |
| d2 = dt_date.fromisoformat(end) | |
| if d2 < d1: | |
| return {"status": "error", "message": "p_end_date must be after p_start_date."} | |
| if (d2 - d1).days > 90: | |
| return {"status": "error", "message": "Date range cannot exceed 90 days for get_market_snapshot."} | |
| except ValueError: | |
| return {"status": "error", "message": "Invalid date format. Use YYYY-MM-DD for p_start_date and p_end_date."} | |
| return { | |
| "p_market": market, | |
| "p_start_date": start, | |
| "p_end_date": end, | |
| } | |
| def _norm_market_trend(args: dict) -> dict: | |
| """Normalizes parameters for get_market_trend.""" | |
| market = _first_val(args, "p_market", "market") | |
| if not market: | |
| return {"status": "error", "message": "p_market is required for get_market_trend. Call get_tracked_markets to see available markets."} | |
| days = _clamp(_first_val(args, "p_days", "days"), 7, 90, default=14) | |
| plat = _first_val(args, "p_platform", "platform") | |
| is_act = args.get("p_is_active") if "p_is_active" in args else True | |
| return { | |
| "p_market": market, | |
| "p_days": days, | |
| "p_platform": str(plat) if plat else None, | |
| "p_is_active": bool(is_act), | |
| } | |
| def _norm_spike_alerts(args: dict) -> dict: | |
| """Normalizes parameters for get_spike_alerts.""" | |
| thresh = float(_first_val(args, "threshold_param", "p_threshold", "threshold", default=25.0)) | |
| days = _clamp(_first_val(args, "days_param", "p_days", "days"), 1, 30, default=7) | |
| market = _first_val(args, "p_market", "market") | |
| limit = _clamp(_first_val(args, "p_limit", "limit"), 1, 15, default=8) | |
| pos = _norm_rank_pos(_first_val(args, "p_rank_position", "rank_position")) | |
| return { | |
| "threshold_param": max(5.0, min(thresh, 100.0)), | |
| "days_param": days, | |
| "p_market": str(market) if market else None, | |
| "p_limit": limit, | |
| "p_rank_position": pos, | |
| } | |
| def _norm_rate_anomaly_report(args: dict) -> dict: | |
| """Normalizes parameters for get_rate_anomaly_report.""" | |
| search = _first_val(args, "p_property_search", "p_search", "p_market", "property_search") | |
| days = _clamp(_first_val(args, "p_days", "days"), 1, 90, default=30) | |
| dev = float(_first_val(args, "p_deviation_threshold", "p_threshold", "deviation_threshold", default=25.0)) | |
| return { | |
| "p_property_search": str(search) if search else None, | |
| "p_days": days, | |
| "p_deviation_threshold": max(5.0, min(dev, 100.0)), | |
| } | |
| def _norm_most_volatile_properties(args: dict) -> dict: | |
| """Normalizes parameters for get_most_volatile_properties.""" | |
| market = _first_val(args, "p_market", "market") | |
| days = _clamp(_first_val(args, "p_days", "days"), 7, 90, default=14) | |
| limit = _clamp(_first_val(args, "p_limit", "limit"), 1, 15, default=5) | |
| pos = _norm_rank_pos(_first_val(args, "p_rank_position", "rank_position")) | |
| return { | |
| "p_market": market, | |
| "p_days": days, | |
| "p_limit": limit, | |
| "p_rank_position": pos, | |
| } | |
| def _norm_property_snapshot(args: dict) -> dict: | |
| """Normalizes parameters for get_property_snapshot.""" | |
| search = _first_val(args, "p_property_search", "p_property_id", "property_search", "property_id") | |
| if not search: | |
| return {"status": "error", "message": "get_property_snapshot requires p_property_search (UUID or property title)."} | |
| return {"p_property_search": str(search)} | |
| def _norm_property_rate_changes(args: dict) -> dict: | |
| """Normalizes parameters for get_property_rate_changes.""" | |
| search = _first_val(args, "property_search", "p_property_search", "p_property_id", "property_id", "p_market", default="") | |
| start = _first_val(args, "start_date", "p_start_date") | |
| end = _first_val(args, "end_date", "p_end_date") | |
| days = _clamp(_first_val(args, "days_param", "p_days", "days"), 1, 90, default=14) | |
| comp = _clamp(args.get("compare_window_days"), 1, 14, default=1) | |
| limit = _clamp(_first_val(args, "p_limit", "limit"), 1, 30, default=14) | |
| return { | |
| "property_search": str(search), | |
| "days_param": days, | |
| "compare_window_days": comp, | |
| "start_date": start, | |
| "end_date": end, | |
| "p_limit": limit, | |
| } | |
| def _norm_compare_properties(args: dict) -> dict: | |
| """Normalizes parameters for compare_properties.""" | |
| ids = _first_val(args, "p_property_ids", "property_ids", default=[]) | |
| if not isinstance(ids, list) or len(ids) < 2: | |
| return {"status": "error", "message": "compare_properties requires a list of 2-10 property names or UUIDs in p_property_ids."} | |
| return {"p_property_ids": ids[:10]} | |
| def _norm_search_properties(args: dict) -> dict: | |
| """Normalizes parameters for search_properties.""" | |
| search = _first_val(args, "p_search", "p_query", "query") | |
| market = _first_val(args, "p_market", "market") | |
| platform = _first_val(args, "p_platform", "platform") | |
| bedrooms = _first_val(args, "p_bedrooms", "bedrooms") | |
| avail = args.get("p_available") if "p_available" in args else args.get("available") | |
| is_act = args.get("p_is_active") if "p_is_active" in args else True | |
| pos = _norm_rank_pos(_first_val(args, "p_rank_position", "rank_position")) | |
| sort_by = _first_val(args, "p_sort_by", "sort_by", default="rate") | |
| limit = _clamp(_first_val(args, "p_limit", "limit"), 1, 20, default=6) | |
| return { | |
| "p_search": str(search) if search else None, | |
| "p_market": str(market) if market else None, | |
| "p_platform": str(platform) if platform else None, | |
| "p_bedrooms": int(bedrooms) if bedrooms is not None else None, | |
| "p_available": bool(avail) if avail is not None else None, | |
| "p_is_active": bool(is_act) if is_act is not None else None, | |
| "p_rank_position": pos, | |
| "p_sort_by": str(sort_by), | |
| "p_limit": limit, | |
| } | |
| def _norm_availability_rate(args: dict) -> dict: | |
| """Normalizes parameters for get_availability_rate.""" | |
| market = _first_val(args, "p_market", "market") | |
| platform = _first_val(args, "p_platform", "platform") | |
| return { | |
| "p_market": str(market) if market else None, | |
| "p_platform": str(platform) if platform else None, | |
| } | |
| def _norm_nearby_properties(args: dict) -> dict: | |
| """Normalizes parameters for get_nearby_properties.""" | |
| lat = _first_val(args, "p_latitude", "latitude") | |
| lon = _first_val(args, "p_longitude", "longitude") | |
| if lat is None or lon is None: | |
| return { | |
| "status": "error", | |
| "message": "get_nearby_properties requires p_latitude and p_longitude. Call geocode_address first if you only have an address." | |
| } | |
| rad_raw = _first_val(args, "p_radius_km", "radius_km", default=5.0) | |
| rad = max(0.1, min(float(rad_raw), 20.0)) | |
| limit = _clamp(_first_val(args, "p_limit", "limit"), 1, 20, default=6) | |
| return { | |
| "p_latitude": float(lat), | |
| "p_longitude": float(lon), | |
| "p_radius_km": rad, | |
| "p_limit": limit, | |
| } | |
| def _norm_distance_km(args: dict) -> dict: | |
| """Normalizes parameters for get_distance_km.""" | |
| p_a = _first_val(args, "property_a_id", "p_from_property_id", "from_property_id") | |
| p_b = _first_val(args, "property_b_id", "p_to_property_id", "to_property_id") | |
| if not is_valid_uuid(p_a) or not is_valid_uuid(p_b): | |
| return {"status": "error", "message": "Invalid UUID format. Both property IDs must be valid UUIDs."} | |
| return { | |
| "property_a_id": p_a, | |
| "property_b_id": p_b, | |
| } | |
| def _norm_tracked_markets(args: dict) -> dict: | |
| """Normalizes parameters for get_tracked_markets.""" | |
| plat = _first_val(args, "p_platform", "platform") | |
| return {"p_platform": str(plat) if plat else None} | |
| def _norm_recently_changed_tracking(args: dict) -> dict: | |
| """Normalizes parameters for get_recently_changed_tracking.""" | |
| days = _clamp(_first_val(args, "p_days", "days"), 1, 90, default=30) | |
| return {"p_days": days} | |
| def _norm_property_detail(args: dict) -> dict: | |
| """Normalizes parameters for get_property_detail.""" | |
| search = _first_val(args, "p_property_search", "property_search", "property_id", "name") | |
| if not search: | |
| return {"status": "error", "message": "get_property_detail requires p_property_search (UUID or property title)."} | |
| days = _clamp(_first_val(args, "p_history_days", "history_days", "days"), 1, 30, default=14) | |
| return { | |
| "p_property_search": str(search), | |
| "p_history_days": days, | |
| } | |
| def _norm_market_rate_changes(args: dict) -> dict: | |
| """Normalizes parameters for get_market_rate_changes.""" | |
| market = _first_val(args, "p_market", "market") | |
| days = _clamp(_first_val(args, "p_days", "days"), 1, 30, default=7) | |
| limit = _clamp(_first_val(args, "p_limit", "limit"), 1, 5, default=5) | |
| return { | |
| "p_market": str(market) if market else None, | |
| "p_days": days, | |
| "p_limit": limit, | |
| } | |
| _TOOL_NORMALIZERS = { | |
| "get_real_estate_kpis": _norm_kpis, | |
| "get_dashboard_kpis": _norm_kpis, | |
| "get_market_averages": _norm_market_averages, | |
| "get_market_snapshot": _norm_market_snapshot, | |
| "get_market_trend": _norm_market_trend, | |
| "get_spike_alerts": _norm_spike_alerts, | |
| "get_rate_anomaly_report": _norm_rate_anomaly_report, | |
| "get_most_volatile_properties": _norm_most_volatile_properties, | |
| "get_property_snapshot": _norm_property_snapshot, | |
| "get_property_rate_changes": _norm_property_rate_changes, | |
| "compare_properties": _norm_compare_properties, | |
| "search_properties": _norm_search_properties, | |
| "get_availability_rate": _norm_availability_rate, | |
| "get_nearby_properties": _norm_nearby_properties, | |
| "get_distance_km": _norm_distance_km, | |
| "get_tracked_markets": _norm_tracked_markets, | |
| "get_recently_changed_tracking": _norm_recently_changed_tracking, | |
| "get_property_detail": _norm_property_detail, | |
| "get_market_rate_changes": _norm_market_rate_changes, | |
| } | |
| async def _call_supabase_rpc(func_name: str, clean_args: dict) -> dict: | |
| """Invokes Supabase RPC with cleaned parameters and formats response.""" | |
| try: | |
| query_params = {k: v for k, v in clean_args.items() if v is not None} | |
| res = supabase.rpc(func_name, query_params).execute() | |
| raw = res.data | |
| if raw is None: | |
| return {"status": "success", "data": []} | |
| if isinstance(raw, dict): | |
| if "status" not in raw: | |
| raw["status"] = "success" | |
| return raw | |
| return {"status": "success", "data": raw} | |
| except Exception as e: | |
| err_msg = str(e) | |
| if "PGRST202" in err_msg: | |
| return { | |
| "status": "error", | |
| "message": f"Tool argument schema mismatch for '{func_name}'. Ensure parameters strictly match pulse_ai_revamped_rpcs.sql." | |
| } | |
| return {"status": "error", "message": err_msg} | |
| async def execute_tool_rpc(func_name: str, args: dict) -> dict: | |
| """Executes a read-only Supabase RPC matching the tool schema.""" | |
| if func_name in _PYTHON_SIDE_TOOLS: | |
| return { | |
| "status": "error", | |
| "message": f"Tool '{func_name}' is handled client-side and must not route to Supabase." | |
| } | |
| normalizer = _TOOL_NORMALIZERS.get(func_name) | |
| if not normalizer: | |
| return { | |
| "status": "error", | |
| "message": f"Unknown tool name: '{func_name}'" | |
| } | |
| clean_args = normalizer(args) | |
| if "status" in clean_args and clean_args["status"] == "error": | |
| return clean_args | |
| return await _call_supabase_rpc(func_name, clean_args) | |
| async def search_methodology_rag(query: str, top_k: int = 3) -> list: | |
| """Embeds query and retrieves top matching methodology documentation chunks.""" | |
| try: | |
| vector = embedder.encode(query).tolist() | |
| res = supabase.rpc( | |
| "match_re_methodology", | |
| { | |
| "query_embedding": vector, | |
| "match_threshold": 0.20, | |
| "match_count": top_k | |
| } | |
| ).execute() | |
| return [f"### {item['section_title']}\n{item['chunk_content']}" for item in res.data] | |
| except Exception as e: | |
| print(f"RAG Retrieval Error: {e}") | |
| return [] | |