diff --git a/pxy_agents_coral/formatters.py b/pxy_agents_coral/formatters.py index 674a1bd..6e2588e 100644 --- a/pxy_agents_coral/formatters.py +++ b/pxy_agents_coral/formatters.py @@ -2,7 +2,8 @@ from __future__ import annotations # import json import re import requests -from typing import Any, Dict, Tuple +from typing import Any, Dict, Tuple, Optional, List +from django.conf import settings from django.http import JsonResponse, HttpRequest from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @@ -18,6 +19,131 @@ def _load_body(request: HttpRequest) -> Dict[str, Any]: except Exception: return {} +_SITES_DEFAULTS = { + "city": "CDMX", + "business": "all", + "time_bands": [10, 20], + "center_by_city": { + "CDMX": (19.4326, -99.1332), + }, +} + +def _parse_time_bands(raw: str) -> list[int]: + bands: list[int] = [] + for part in re.split(r"[,\s]+", (raw or "").strip()): + if not part: + continue + try: + val = int(part) + except Exception: + continue + if val > 0: + bands.append(val) + return bands + +def _parse_latlon(raw: str) -> Optional[List[float]]: + parts = [p.strip() for p in (raw or "").split(",")] + if len(parts) != 2: + return None + try: + lat = float(parts[0]) + lon = float(parts[1]) + except Exception: + return None + return [lat, lon] + +def _parse_sites_shorthand(body: Dict[str, Any]) -> Tuple[Dict[str, Any], str]: + args_raw = (body.get("input", {}) or {}).get("args_raw") or "" + cleaned = re.sub(r"^/\w+\s*", "", args_raw).strip() + if not cleaned: + return {}, "empty" + + payload: Dict[str, Any] = {} + time_bands: list[int] = [] + lat_val = None + lon_val = None + + for tok in cleaned.split(): + key = None + val = None + if "=" in tok: + key, val = tok.split("=", 1) + elif ":" in tok: + key, val = tok.split(":", 1) + + if key is not None: + key = key.strip().lower() + val = (val or "").strip() + if key in {"city", "c"}: + payload["city"] = val + elif key in {"business", "biz", "b", "category", "cat"}: + payload["business"] = val + elif key in {"time", "times", "band", "bands", "time_bands", "tb"}: + bands = _parse_time_bands(val) + if bands: + time_bands.extend(bands) + elif key in {"max", "max_candidates", "k", "top"}: + try: + payload["max_candidates"] = max(1, int(val)) + except Exception: + pass + elif key in {"center", "ctr"}: + latlon = _parse_latlon(val) + if latlon: + payload["center"] = latlon + elif key in {"lat", "latitude"}: + try: + lat_val = float(val) + except Exception: + pass + elif key in {"lon", "lng", "longitude"}: + try: + lon_val = float(val) + except Exception: + pass + continue + + if re.fullmatch(r"\d+(?:,\d+)*", tok): + bands = _parse_time_bands(tok) + if bands: + time_bands.extend(bands) + continue + + if tok.isalpha() and tok.isupper() and "city" not in payload: + payload["city"] = tok + continue + + if "business" not in payload: + payload["business"] = tok + else: + payload["business"] = f"{payload['business']}_{tok}" + + if time_bands: + payload["time_bands"] = time_bands + if lat_val is not None and lon_val is not None and "center" not in payload: + payload["center"] = [lat_val, lon_val] + + return payload, "shorthand" + +def _apply_sites_defaults(payload: Dict[str, Any], body: Dict[str, Any]) -> Dict[str, Any]: + out = dict(payload or {}) + loc = (body.get("input") or {}).get("location") or {} + if "center" not in out and loc.get("lat") is not None and loc.get("lon") is not None: + out["center"] = [loc.get("lat"), loc.get("lon")] + + city = (out.get("city") or _SITES_DEFAULTS["city"]).strip() + out["city"] = city + if not out.get("business"): + out["business"] = _SITES_DEFAULTS["business"] + if not out.get("time_bands"): + out["time_bands"] = list(_SITES_DEFAULTS["time_bands"]) + + if "center" not in out: + fallback = _SITES_DEFAULTS["center_by_city"].get(city.upper()) + if fallback: + out["center"] = [fallback[0], fallback[1]] + return out + def _extract_payload(body: Dict[str, Any]) -> Tuple[Dict[str, Any], str]: """ Returns (payload, src) where src is 'payload', 'args_raw', or 'empty'. @@ -41,8 +167,19 @@ def _extract_payload(body: Dict[str, Any]) -> Tuple[Dict[str, Any], str]: return {}, "empty" +def _resolve_internal_base(request: HttpRequest) -> str: + base = (getattr(settings, "AGENTS_INTERNAL_BASE", "") or "").strip() + if base: + return base.rstrip("/") + host = (request.get_host() or "").lower() + if host.endswith(":8011"): + return "http://127.0.0.1:8000" + if host.endswith(":8010"): + return "http://127.0.0.1:8002" + return "http://127.0.0.1:8002" + def _post_execute(request: HttpRequest, agent: str, payload: Dict[str, Any], timeout: float = 30.0): - url = f"{_base(request)}/api/agents/execute" + url = f"{_resolve_internal_base(request)}/api/agents/execute" try: r = requests.post(url, json={"agent": agent, "payload": payload}, timeout=timeout) # try parse json regardless of status @@ -107,6 +244,9 @@ def format_sami(request: HttpRequest): def format_sites(request: HttpRequest): body = _load_body(request) payload, src = _extract_payload(body) + if not payload and src == "empty": + payload, src = _parse_sites_shorthand(body) + payload = _apply_sites_defaults(payload, body) status, data = _post_execute(request, "sites", payload, timeout=30.0) data = data if isinstance(data, dict) else {"result": data} data.setdefault("_echo", {"src": src, "payload_keys": list(payload.keys())}) diff --git a/pxy_agents_coral/views.py b/pxy_agents_coral/views.py index 1ca8db5..87383c7 100644 --- a/pxy_agents_coral/views.py +++ b/pxy_agents_coral/views.py @@ -25,6 +25,21 @@ except Exception: # For the generic /api/agents/execute proxy (kept for compatibility) AGENTS_INTERNAL_BASE = getattr(settings, "AGENTS_INTERNAL_BASE", "") +def _resolve_internal_base(request: HttpRequest) -> str: + base = (AGENTS_INTERNAL_BASE or "").strip() + if base: + return base.rstrip("/") + host = (request.get_host() or "").lower() + if host.endswith(":8011"): + return "http://127.0.0.1:8000" + if host.endswith(":8010"): + return "http://127.0.0.1:8002" + if host.endswith(":8000"): + return "http://127.0.0.1:8000" + if host.endswith(":8002"): + return "http://127.0.0.1:8002" + return "http://127.0.0.1:8002" + # For the formatter endpoints we *force* an internal base and never guess from Host. # Set in .env: AGENTS_INTERNAL_BASE=http://127.0.0.1:8002 # Fallback keeps you safe even if env is missing/misread. @@ -188,7 +203,7 @@ def agents_execute(request: HttpRequest): return JsonResponse({"code": "BAD_REQUEST", "message": "missing 'payload'"}, status=400) path = "/api/sami/run" if agent == "sami" else "/api/sites/search" - base = (AGENTS_INTERNAL_BASE or "http://127.0.0.1:8002").rstrip("/") + base = _resolve_internal_base(request) url = f"{base}{path}" r = requests.post(url, json=payload, timeout=90) @@ -255,4 +270,3 @@ def agents_health(request): except Exception as e: data["checks"]["sites"] = {"ok": False, "error": str(e)} return JsonResponse(data) -