Views with no timeout due to 8000 port
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Ekaropolus 2025-09-18 20:03:09 -06:00
parent 438fd1c313
commit ab277f7628

View File

@ -1,30 +1,114 @@
# polisplexity/pxy_agents_coral/views.py # polisplexity/pxy_agents_coral/views.py
from __future__ import annotations from __future__ import annotations
import json import json
import requests import re
from typing import Any, Dict, Tuple
import requests
from django.conf import settings from django.conf import settings
from django.http import JsonResponse from django.http import JsonResponse, HttpRequest
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods, require_POST from django.views.decorators.http import require_http_methods, require_POST
# -------------------------------------------------
# Contracts spec version (best-effort import) # Contracts spec version (best-effort import)
# -------------------------------------------------
try: try:
from pxy_contracts.version import SPEC_VERSION from pxy_contracts.version import SPEC_VERSION
except Exception: except Exception:
SPEC_VERSION = "0.1.0" SPEC_VERSION = "0.1.0"
# Where to call your internal APIs from inside the container. # Where to call your internal APIs for /api/agents/execute proxying.
# In prod you likely want: "http://127.0.0.1:8002" # In prod: set AGENTS_INTERNAL_BASE=http://127.0.0.1:8002
AGENTS_INTERNAL_BASE = getattr(settings, "AGENTS_INTERNAL_BASE", "") AGENTS_INTERNAL_BASE = getattr(settings, "AGENTS_INTERNAL_BASE", "")
# -------------------------------------------------
# Small helpers shared by formatter endpoints
# -------------------------------------------------
def _base(request: HttpRequest) -> str:
"""Absolute base URL without trailing slash."""
return request.build_absolute_uri("/")[:-1]
def _load_body(request: HttpRequest) -> Dict[str, Any]:
try:
raw = (request.body or b"").decode("utf-8")
return json.loads(raw or "{}")
except Exception:
return {}
def _extract_payload(body: Dict[str, Any]) -> Tuple[Dict[str, Any], str]:
"""
Returns (payload, src) where src is 'payload', 'args_raw', or 'empty'.
Accepts:
1) {"payload": {...}}
2) Canonical tg envelope with .input.args_raw like "/sami {...}"
"""
if isinstance(body.get("payload"), dict):
return body["payload"], "payload"
args_raw = (body.get("input", {}) or {}).get("args_raw") or ""
cleaned = re.sub(r"^/\w+\s*", "", args_raw).strip()
if cleaned:
try:
return json.loads(cleaned), "args_raw"
except Exception:
pass
return {}, "empty"
def _post_underlying(request: HttpRequest, agent: str, payload: Dict[str, Any], timeout: float = 60.0):
"""
Directly call the real internal APIs on THIS host:8002, bypassing the execute proxy.
sami -> /api/sami/run
sites -> /api/sites/search
"""
path = "/api/sami/run" if agent == "sami" else "/api/sites/search"
url = f"{_base(request)}{path}"
try:
r = requests.post(url, json=payload, timeout=timeout)
try:
data = r.json()
except Exception:
data = {"code": "NON_JSON", "message": r.text[:2000]}
return r.status_code, data
except requests.Timeout:
return 504, {"code": "UPSTREAM_TIMEOUT", "message": "agent upstream timed out"}
except Exception as e:
return 500, {"code": "EXEC_ERROR", "message": str(e)}
# Tiny text builders (handy for bots)
def _text_sami(data: Dict[str, Any]) -> str:
if "beta" in data and "r2" in data:
lines = [f"SAMI run: β={data['beta']:.3f}, R²={data['r2']:.3f}"]
for c in sorted(data.get("residuals", []), key=lambda x: x.get("rank", 1e9))[:3]:
lines.append(f"{c.get('rank')}. {c.get('city')}: {c.get('sami',0):+0.2f}")
if data.get("share_url"):
lines += ["", data["share_url"]]
return "\n".join(lines)
if data.get("code"):
return f"⚠️ {data.get('code')}: {data.get('message','')}"
return "SAMI results ready."
def _text_sites(data: Dict[str, Any]) -> str:
if isinstance(data.get("candidates"), list):
city = data.get("city", "?"); biz = data.get("business", "?")
lines = [f"Top sites for {biz} in {city}:"]
for i, c in enumerate(data["candidates"][:3], 1):
lines.append(f"{i}. score={c.get('score',0):.2f} @ ({c.get('lat',0):.5f},{c.get('lon',0):.5f})")
for k in ("share_url","isochrones_geojson_url","candidates_geojson_url"):
if data.get(k): lines.append(data[k])
return "\n".join(lines)
if data.get("code"):
return f"⚠️ {data.get('code')}: {data.get('message','')}"
return "Site scoring ready."
# -------------------------------------------------
# Public endpoints
# -------------------------------------------------
@csrf_exempt @csrf_exempt
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def agents_list(request): def agents_list(request: HttpRequest):
base = request.build_absolute_uri("/")[:-1] base = _base(request)
agents = [ agents = [
{ {
"agent": "sami", "agent": "sami",
@ -46,33 +130,32 @@ def agents_list(request):
}, },
] ]
# 👇 add a simple text summary the bot can send
lines = ["Available agents:"] lines = ["Available agents:"]
for a in agents: for a in agents:
lines.append(f"- {a['agent']}: {a['description']}") lines.append(f"- {a['agent']}: {a['description']}")
lines.append("") lines += [
lines.append("Try:") "",
lines.append('/sami {"indicator":"imss_wages_2023","cities":["CDMX","GDL","MTY"]}') "Try:",
lines.append('/sites {"city":"CDMX","business":"cafe","time_bands":[10,20]}') '/sami {"indicator":"imss_wages_2023","cities":["CDMX","GDL","MTY"]}',
'/sites {"city":"CDMX","business":"cafe","time_bands":[10,20]}',
]
return JsonResponse({"agents": agents, "text": "\n".join(lines)}) return JsonResponse({"agents": agents, "text": "\n".join(lines)})
@csrf_exempt @csrf_exempt
@require_POST @require_POST
def agents_execute(request): def agents_execute(request: HttpRequest):
""" """
POST /api/agents/execute POST /api/agents/execute
Body: { "agent": "sami"|"sites", "payload": {...} } Body: { "agent": "sami"|"sites", "payload": {...} }
Proxies to: /api/sami/run or /api/sites/search Proxies to: /api/sami/run or /api/sites/search
(Uses AGENTS_INTERNAL_BASE if set; else same-host absolute URL.)
""" """
try: try:
body = json.loads(request.body.decode("utf-8") or "{}") body = json.loads(request.body.decode("utf-8") or "{}")
agent = (body.get("agent") or "").strip().lower() agent = (body.get("agent") or "").strip().lower()
payload = body.get("payload") payload = body.get("payload")
if agent not in ("sami", "sites"): if agent not in {"sami", "sites"}:
return JsonResponse( return JsonResponse(
{"code": "AGENT_NOT_FOUND", "message": f"unknown agent '{agent}'"}, {"code": "AGENT_NOT_FOUND", "message": f"unknown agent '{agent}'"},
status=404, status=404,
@ -83,20 +166,12 @@ def agents_execute(request):
status=400, status=400,
) )
# Resolve proxy target
path = "/api/sami/run" if agent == "sami" else "/api/sites/search" path = "/api/sami/run" if agent == "sami" else "/api/sites/search"
# Prefer explicit internal base; otherwise same-host absolute URL
url = (AGENTS_INTERNAL_BASE or "").rstrip("/") + path url = (AGENTS_INTERNAL_BASE or "").rstrip("/") + path
if not url.startswith("http"): if not url.startswith("http"): # fall back to same host:port (8002)
base = request.build_absolute_uri("/")[:-1] url = f"{_base(request)}{path}"
url = f"{base}{path}"
# Proxy
r = requests.post(url, json=payload, timeout=90) r = requests.post(url, json=payload, timeout=90)
# Pass through JSON (may be list/dict) & status code
# Use safe=False because upstream could return a list
return JsonResponse(r.json(), status=r.status_code, safe=False) return JsonResponse(r.json(), status=r.status_code, safe=False)
except requests.Timeout: except requests.Timeout:
@ -105,11 +180,37 @@ def agents_execute(request):
status=504, status=504,
) )
except ValueError as ve: except ValueError as ve:
# Bad JSON in request or from upstream
return JsonResponse({"code": "BAD_JSON", "message": str(ve)}, status=400) return JsonResponse({"code": "BAD_JSON", "message": str(ve)}, status=400)
except Exception as e: except Exception as e:
# Last-resort error envelope return JsonResponse({"code": "AGENT_EXEC_ERROR", "message": str(e)}, status=500)
return JsonResponse(
{"code": "AGENT_EXEC_ERROR", "message": str(e)}, # -------------------------------------------------
status=500, # Formatter endpoints (call underlying APIs directly)
) # -------------------------------------------------
@csrf_exempt
@require_http_methods(["GET", "POST"])
def format_sami(request: HttpRequest):
body = _load_body(request)
payload, src = _extract_payload(body)
status, data = _post_underlying(request, "sami", payload, timeout=60.0)
data = data if isinstance(data, dict) else {"result": data}
data.setdefault("_echo", {"src": src, "payload_keys": list(payload.keys())})
try:
data["text"] = _text_sami(data)
except Exception:
pass
return JsonResponse(data, status=status, safe=False)
@csrf_exempt
@require_http_methods(["GET", "POST"])
def format_sites(request: HttpRequest):
body = _load_body(request)
payload, src = _extract_payload(body)
status, data = _post_underlying(request, "sites", payload, timeout=60.0)
data = data if isinstance(data, dict) else {"result": data}
data.setdefault("_echo", {"src": src, "payload_keys": list(payload.keys())})
try:
data["text"] = _text_sites(data)
except Exception:
pass
return JsonResponse(data, status=status, safe=False)