diff --git a/.gitignore b/.gitignore index 402cda9..8f17090 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ docker-compose.override.yml docker-compose.override.yml pxy_meta_pages.zip pxy_openai.zip +pxy_bots.zip +pxy_bots (2).zip +pxy_bots.zip +pxy_bots.zip diff --git a/polisplexity/settings.py b/polisplexity/settings.py index 27bf620..30db852 100644 --- a/polisplexity/settings.py +++ b/polisplexity/settings.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ "rest_framework", "pxy_api", + 'pxy_agents_coral', # Third-party apps @@ -215,3 +216,5 @@ REST_FRAMEWORK = { # Manejo de errores uniforme "EXCEPTION_HANDLER": "pxy_api.exceptions.envelope_exception_handler", } + +AGENTS_INTERNAL_BASE = "http://127.0.0.1:8000" diff --git a/polisplexity/urls.py b/polisplexity/urls.py index 7ba4c81..bb91889 100644 --- a/polisplexity/urls.py +++ b/polisplexity/urls.py @@ -50,6 +50,10 @@ urlpatterns = [ path("", include("pxy_openai.urls")), + path("", include("pxy_contracts.urls")), + + path('', include('pxy_agents_coral.urls')), + ] diff --git a/pxy_agents_coral/__init__.py b/pxy_agents_coral/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pxy_agents_coral/admin.py b/pxy_agents_coral/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/pxy_agents_coral/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/pxy_agents_coral/apps.py b/pxy_agents_coral/apps.py new file mode 100644 index 0000000..db1557e --- /dev/null +++ b/pxy_agents_coral/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PxyAgentsCoralConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pxy_agents_coral' diff --git a/pxy_agents_coral/migrations/__init__.py b/pxy_agents_coral/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pxy_agents_coral/models.py b/pxy_agents_coral/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/pxy_agents_coral/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/pxy_agents_coral/tests.py b/pxy_agents_coral/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/pxy_agents_coral/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/pxy_agents_coral/urls.py b/pxy_agents_coral/urls.py new file mode 100644 index 0000000..451f330 --- /dev/null +++ b/pxy_agents_coral/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('api/agents/list', views.agents_list, name='agents_list'), + path('api/agents/execute', views.agents_execute, name='agents_execute'), +] diff --git a/pxy_agents_coral/views.py b/pxy_agents_coral/views.py new file mode 100644 index 0000000..138ad00 --- /dev/null +++ b/pxy_agents_coral/views.py @@ -0,0 +1,98 @@ +from django.http import JsonResponse +from django.views.decorators.http import require_GET + +try: + from pxy_contracts.version import SPEC_VERSION +except Exception: + SPEC_VERSION = '0.1.0' + +@require_GET +def agents_list(request): + base = request.build_absolute_uri('/')[:-1] # absolute base, no trailing slash + agents = [ + { + 'agent': 'sami', + 'name': 'SAMI-Agent', + 'version': '1.0.0', + 'spec_version': SPEC_VERSION, + 'contracts_url': f'{base}/api/contracts/sami.json', + 'execute_url': f'{base}/api/agents/execute', + 'description': 'Urban scaling (β,R²) + SAMI residuals + chart', + }, + { + 'agent': 'sites', + 'name': 'Sites-Agent', + 'version': '1.0.0', + 'spec_version': SPEC_VERSION, + 'contracts_url': f'{base}/api/contracts/sites.json', + 'execute_url': f'{base}/api/agents/execute', + 'description': 'Site scoring (access, demand, competition) with maps', + }, + ] + return JsonResponse({'agents': agents}) + + +import json +import requests +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST +from django.http import JsonResponse + +from django.conf import settings +# Internal base where your existing APIs live (same host in MVP) +AGENTS_INTERNAL_BASE = getattr(settings, "AGENTS_INTERNAL_BASE", "") # empty = use same host via relative path + +@csrf_exempt +@require_POST +def agents_execute(request): + """ + POST /api/agents/execute + Body: { "agent": "sami"|"sites", "payload": {...} } + Proxies to: /api/sami/run or /api/sites/search + """ + try: + body = json.loads(request.body.decode("utf-8")) + agent = (body.get("agent") or "").strip().lower() + payload = body.get("payload") + if agent not in ("sami", "sites"): + return JsonResponse( + {"code": "AGENT_NOT_FOUND", "message": f"unknown agent '{agent}'"}, + status=404, + ) + if payload is None: + return JsonResponse( + {"code": "BAD_REQUEST", "message": "missing 'payload'"}, + status=400, + ) + + # Resolve proxy target + if agent == "sami": + path = "/api/sami/run" + else: + path = "/api/sites/search" + + url = (AGENTS_INTERNAL_BASE or "").rstrip("/") + path + # Absolute self-call (same container) using the request host + if not url.startswith("http"): + base = request.build_absolute_uri("/")[:-1] + url = f"{base}{path}" + + r = requests.post(url, json=payload, timeout=90) + # Pass through JSON and status code + return JsonResponse(r.json(), status=r.status_code, safe=False) + + except requests.Timeout: + return JsonResponse( + {"code": "UPSTREAM_TIMEOUT", "message": "agent upstream timed out"}, + status=504, + ) + except ValueError as ve: + return JsonResponse( + {"code": "BAD_JSON", "message": str(ve)}, + status=400, + ) + except Exception as e: + return JsonResponse( + {"code": "AGENT_EXEC_ERROR", "message": str(e)}, + status=500, + ) diff --git a/pxy_bots (2).zip b/pxy_bots (2).zip deleted file mode 100644 index 399022d..0000000 Binary files a/pxy_bots (2).zip and /dev/null differ diff --git a/pxy_bots.zip b/pxy_bots.zip index f438163..6823993 100644 Binary files a/pxy_bots.zip and b/pxy_bots.zip differ diff --git a/pxy_contracts/urls.py b/pxy_contracts/urls.py new file mode 100644 index 0000000..1c6d0b6 --- /dev/null +++ b/pxy_contracts/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("api/contracts/sami.json", views.sami_contracts, name="contracts_sami"), + path("api/contracts/sites.json", views.sites_contracts, name="contracts_sites"), +] diff --git a/pxy_contracts/views.py b/pxy_contracts/views.py index 91ea44a..2b03ce9 100644 --- a/pxy_contracts/views.py +++ b/pxy_contracts/views.py @@ -1,3 +1,40 @@ -from django.shortcuts import render +from django.http import JsonResponse +from django.views.decorators.http import require_GET -# Create your views here. +# Versión del contrato +try: + from .version import SPEC_VERSION +except Exception: + SPEC_VERSION = "0.1.0" + +# Modelos Pydantic +from .contracts.sami import SAMIRunRequest, SAMIRunResponse +from .contracts.sites import SiteSearchRequest, SiteSearchResponse + + +def _schema_of(model_cls): + """Devuelve JSONSchema para Pydantic v2 o v1.""" + try: + return model_cls.model_json_schema() # Pydantic v2 + except Exception: + return model_cls.schema() # Pydantic v1 + + +@require_GET +def sami_contracts(request): + """GET /api/contracts/sami.json""" + return JsonResponse({ + "spec_version": SPEC_VERSION, + "request": _schema_of(SAMIRunRequest), + "response": _schema_of(SAMIRunResponse), + }) + + +@require_GET +def sites_contracts(request): + """GET /api/contracts/sites.json""" + return JsonResponse({ + "spec_version": SPEC_VERSION, + "request": _schema_of(SiteSearchRequest), + "response": _schema_of(SiteSearchResponse), + }) diff --git a/pxy_dashboard/middleware.py b/pxy_dashboard/middleware.py index d66b470..c580abb 100644 --- a/pxy_dashboard/middleware.py +++ b/pxy_dashboard/middleware.py @@ -113,8 +113,16 @@ EXEMPT_URLS += [ re.compile(r"^api/openai/voice_chat$"), ] +# Contracts (public for interop/coral tooling) +EXEMPT_URLS += [ + re.compile(r"^api/contracts/(sami|sites)\.json$"), +] - +# Coral-style agents catalog & execute (public for MVP) +EXEMPT_URLS += [ + re.compile(r"^api/agents/list$"), + re.compile(r"^api/agents/execute$"), +]