refactor: rename API and frontend projects to Vocero
Rename the project directories, Python distribution and import package, and npm package. Update launcher, Docker, tooling, tests, lockfiles, and documentation while preserving existing API contracts. Make AGENTS.md self-contained and remove external skill and workflow references.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user