chore(vocero): remove the legacy ApiPilot integration (#3)

Vocero now retrieves AquaPortal content through authenticated MCP. This removes the obsolete ApiPilot integration from the API and browser while preserving the separately deployed speech pipeline.

Release notes:

- Removed ApiPilot endpoints, configuration, credential overrides, and HTTP/realtime tool execution.
- Removed browser ApiPilot catalog loading and prompt generation while retaining authenticated MCP discovery.
- Updated regression coverage and operating documentation for the retired routes and settings.

Reviewed-on: AquaPortal/s2s#3
This commit is contained in:
2026-10-08 11:23:41 -04:00
parent 0311d3f1ff
commit dfed03ac7c
39 changed files with 198 additions and 3314 deletions
@@ -1,151 +0,0 @@
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 settings.S2S_MCP_URL:
raise HTTPException(409, "Las herramientas de AquaPortal usan la sesión de conversación.")
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 settings.S2S_MCP_URL:
raise HTTPException(409, "Las herramientas de AquaPortal usan la sesión de conversación.")
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)
+3 -10
View File
@@ -21,12 +21,7 @@ async def config(request: Request) -> JSONResponse:
"""Client bootstrap: whether web search is available, whether the deploy runs
behind a load balancer (so the browser uses the /api/session proxy),
and whether the user may instead set a direct
s2s server URL. The LB address itself is intentionally NOT included.
2026-09-24: new top-level `apipilot` flag. When True the deploy exposes the
new `/api/apipilot/tools` and `/api/apipilot/execute` proxy endpoints (see
docs/client/tool-calling.md) that the browser may call directly instead of
hardcoding upstream tool URLs / names."""
s2s server URL. The LB address itself is intentionally NOT included."""
app_logging.logger.debug(
"[INTERNAL] /api/config requested client=%s", request.client.host if request.client else "?"
)
@@ -42,7 +37,6 @@ async def config(request: Request) -> JSONResponse:
"debugPrompt": settings.S2S_DEBUG_PROMPT,
"startSessionMode": settings.S2S_START_SESSION_MODE,
"theme": settings.S2S_THEME,
"apipilot": bool(settings.S2S_APIPILOT_TOOLS_URL),
}
if settings.S2S_MCP_URL:
if settings.S2S_REASONING_TYPE == "PROXY" and (
@@ -50,13 +44,12 @@ async def config(request: Request) -> JSONResponse:
):
raise HTTPException(503, "MCP en modo PROXY requiere la conexión de voz a través de Vocero.")
out["mcp"] = True
out["apipilot"] = False
app_logging.logger.debug(
"[INTERNAL] /api/config returned promptLen=%d reasoning=%s startSessionMode=%s theme=%s apipilot=%s",
"[INTERNAL] /api/config returned promptLen=%d reasoning=%s startSessionMode=%s theme=%s mcp=%s",
len(primary_prompt),
settings.S2S_REASONING_TYPE,
settings.S2S_START_SESSION_MODE or "-",
settings.S2S_THEME or "-",
out["apipilot"],
bool(settings.S2S_MCP_URL),
)
return validated_json_response(out, ConfigResponse)