"""Browser-owned conversations; backend credentials never cross this boundary.""" import secrets from fastapi import APIRouter, HTTPException, Request, Response from vocero_api.core import config as settings from vocero_api.models.mcp import ConversationRequest, ConversationResponse, IntegrationError, McpCatalogResponse from vocero_api.providers import backend_login, mcp from vocero_api.services import conversations router = APIRouter() def require_mcp() -> None: if not settings.S2S_MCP_URL: raise HTTPException(404, "No encontrado.") @router.post("/api/conversations", response_model=ConversationResponse) async def create_conversation(body: ConversationRequest, request: Request, response: Response) -> ConversationResponse: require_mcp() try: conversations.check_origin(request) login = await backend_login.login(body.phoneNumber.strip()) browser = request.cookies.get(conversations.COOKIE_NAME) or secrets.token_urlsafe(32) item = conversations.store.create(browser, login) except IntegrationError as exc: raise HTTPException(exc.status, exc.message) from None response.set_cookie( conversations.COOKIE_NAME, browser, httponly=True, secure=request.url.scheme == "https" or request.headers.get("x-forwarded-proto") == "https", samesite="strict", ) response.headers["Cache-Control"] = "no-store" return ConversationResponse( conversationId=item.id, expiresIn=settings.S2S_CONVERSATION_TTL_SEC, profile=item.profile ) @router.delete("/api/conversations/current", status_code=204) async def end_conversation(request: Request) -> Response: require_mcp() try: item = conversations.resolve(request) conversations.store.remove(item.id) except IntegrationError as exc: raise HTTPException(exc.status, exc.message) from None return Response(status_code=204) @router.post("/api/conversations/current/interrupt", status_code=204) async def interrupt_conversation(request: Request) -> Response: require_mcp() try: item = conversations.resolve(request) if request.headers.get("x-vocero-turn") == str(item.client_turn): item.interrupt() except IntegrationError as exc: raise HTTPException(exc.status, exc.message) from None return Response(status_code=204) @router.get("/api/mcp/tools", response_model=McpCatalogResponse) async def list_tools(request: Request, response: Response) -> McpCatalogResponse: require_mcp() item: conversations.Conversation | None = None try: item = conversations.resolve(request) catalog = await mcp.discover(item.backend_token) item.require_active() item.catalog = catalog response.headers["Cache-Control"] = "no-store" return McpCatalogResponse(tools=catalog) except IntegrationError as exc: if exc.status == 401 and item is not None: conversations.store.remove(item.id) raise HTTPException(exc.status, exc.message) from None