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
from __future__ import annotations
import json
import requests
import re
from typing import Any, Dict, Tuple
import requests
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.http import require_http_methods, require_POST
# -------------------------------------------------
# Contracts spec version (best-effort import)
# -------------------------------------------------
try:
from pxy_contracts.version import SPEC_VERSION
except Exception:
SPEC_VERSION = "0.1.0"
# Where to call your internal APIs from inside the container.
# In prod you likely want: "http://127.0.0.1:8002"
# Where to call your internal APIs for /api/agents/execute proxying.
# In prod: set AGENTS_INTERNAL_BASE=http://127.0.0.1:8002
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
@require_http_methods(["GET", "POST"])
def agents_list(request):
base = request.build_absolute_uri("/")[:-1]
def agents_list(request: HttpRequest):
base = _base(request)
agents = [
{
"agent": "sami",
@ -46,33 +130,32 @@ def agents_list(request):
},
]
# 👇 add a simple text summary the bot can send
lines = ["Available agents:"]
for a in agents:
lines.append(f"- {a['agent']}: {a['description']}")
lines.append("")
lines.append("Try:")
lines.append('/sami {"indicator":"imss_wages_2023","cities":["CDMX","GDL","MTY"]}')
lines.append('/sites {"city":"CDMX","business":"cafe","time_bands":[10,20]}')
lines += [
"",
"Try:",
'/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)})
@csrf_exempt
@require_POST
def agents_execute(request):
def agents_execute(request: HttpRequest):
"""
POST /api/agents/execute
Body: { "agent": "sami"|"sites", "payload": {...} }
Proxies to: /api/sami/run or /api/sites/search
(Uses AGENTS_INTERNAL_BASE if set; else same-host absolute URL.)
"""
try:
body = json.loads(request.body.decode("utf-8") or "{}")
agent = (body.get("agent") or "").strip().lower()
payload = body.get("payload")
if agent not in ("sami", "sites"):
if agent not in {"sami", "sites"}:
return JsonResponse(
{"code": "AGENT_NOT_FOUND", "message": f"unknown agent '{agent}'"},
status=404,
@ -83,20 +166,12 @@ def agents_execute(request):
status=400,
)
# Resolve proxy target
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
if not url.startswith("http"):
base = request.build_absolute_uri("/")[:-1]
url = f"{base}{path}"
if not url.startswith("http"): # fall back to same host:port (8002)
url = f"{_base(request)}{path}"
# Proxy
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)
except requests.Timeout:
@ -105,11 +180,37 @@ def agents_execute(request):
status=504,
)
except ValueError as ve:
# Bad JSON in request or from upstream
return JsonResponse({"code": "BAD_JSON", "message": str(ve)}, status=400)
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)