from __future__ import annotations from fastapi import APIRouter, HTTPException, Request from fastapi.responses import JSONResponse from vocero_api.api.responses import validated_json_response from vocero_api.core import config as settings from vocero_api.core import logging as app_logging from vocero_api.models import requests as models_requests from vocero_api.models.responses import ToolCatalogResponse, ToolExecutionResponse from vocero_api.providers import apipilot as providers_apipilot router = APIRouter() @router.get("/api/apipilot/tools", response_model=ToolCatalogResponse) async def apipilot_list_tools( request: Request, include_unauthorized: bool = True, refresh: bool = False, ) -> JSONResponse: """Public proxy for ApiPilot tool discovery (guide §2.1). Forwards Authorization / x-demo-session headers from the caller to the upstream backend verbatim and returns the list of `ToolDefinition` records exactly as documented: { "name": "...", "description": "...", "parameters": {...}, "requires_auth": true/false } A short in-process TTL cache is applied (default 5 min, configurable via S2S_APIPILOT_TTL_SEC). The client may force a refresh via `?refresh=true` (e.g. after a user just logged in and previously saw only requires_auth tools).""" if not settings.S2S_APIPILOT_TOOLS_URL: return validated_json_response( model=ToolCatalogResponse, status_code=501, content={ "success": False, "data": None, "error": { "code": "APIPILOT_NOT_CONFIGURED", "message": "ApiPilot no está disponible en este despliegue (S2S_APIPILOT_TOOLS_URL no establecido).", "status_code": 501, "raw": None, }, }, ) auth, demo = providers_apipilot._apipilot_parse_authorization(request) try: tools = await providers_apipilot._apipilot_fetch_tools( auth_header=auth, demo_session=demo, include_unauthorized=bool(include_unauthorized), force_refresh=bool(refresh), ) except HTTPException as exc: # Normalize to envelope-style payload so downstream code is uniform. detail = ( exc.detail if isinstance(exc.detail, dict) else { "code": "APIPILOT_ERROR", "message": str(exc.detail) if exc.detail else "Error", "status_code": exc.status_code, "raw": None, } ) return validated_json_response( model=ToolCatalogResponse, status_code=exc.status_code, content={"success": False, "data": None, "error": detail}, ) app_logging.logger.info( "[APIPILOT] /api/apipilot/tools OK tools=%d include_unauthorized=%s refresh=%s", len(tools), bool(include_unauthorized), bool(refresh), ) return validated_json_response( { "success": True, "data": { "count": len(tools), "include_unauthorized": bool(include_unauthorized), "tools": tools, }, "error": None, }, ToolCatalogResponse, ) @router.post("/api/apipilot/execute", response_model=ToolExecutionResponse) async def apipilot_execute(request: Request, body: models_requests.ApipilotExecuteRequest) -> JSONResponse: """Public proxy for ApiPilot /execute (guide §2.2). Accepts `{name, arguments}`, runs the mandatory PRE-CHECK flow (fresh tool discovery, `requires_auth` gate, strict JSON-Schema argument validation), forwards the request to the upstream backend, and returns the envelope `{success, data, error}`. Show only `error.message` to the end user. `error.code`, `error.status_code` and `error.raw` are for internal auditing (guide §2.2, recommended UI errors).""" if not settings.S2S_APIPILOT_TOOLS_URL: return validated_json_response( model=ToolExecutionResponse, status_code=501, content={ "success": False, "data": None, "error": { "code": "APIPILOT_NOT_CONFIGURED", "message": "ApiPilot no está disponible en este despliegue (S2S_APIPILOT_TOOLS_URL no establecido).", "status_code": 501, "raw": None, }, }, ) auth, demo = providers_apipilot._apipilot_parse_authorization(request) app_logging.logger.info( "[APIPILOT-CALL-NAME] USE=API_ENDPOINT_EXECUTE name=%r argsKeys=%s authReceived=%s", (body.name or ""), sorted(list((body.arguments or {}).keys())) if isinstance(body.arguments, dict) else [], ("Y" if auth or demo else "none"), ) envelope = await providers_apipilot._apipilot_execute_tool( auth_header=auth, demo_session=demo, name=body.name, arguments=body.arguments if isinstance(body.arguments, dict) else {}, ) status_code = 200 if (not envelope.get("success")) and isinstance(envelope.get("error"), dict): code = envelope["error"].get("status_code") try: code_int = int(code) if 400 <= code_int < 599: status_code = code_int except Exception: pass app_logging.logger.info( "[APIPILOT] /api/apipilot/execute name=%s success=%s status=%s", body.name, envelope.get("success"), status_code, ) return validated_json_response(envelope, ToolExecutionResponse, status_code=status_code)