Opening a report or podcast link now establishes an authenticated starting reference for the conversation. Users can select other accessible content or compare reports while retaining the original link, and TALKFIRST greetings can name the verified report or podcast. Release notes: - Added authenticated report and episode link resolution with bounded lookups and unavailable-content handling - Added explicit conversation selection without restricting searches or changing the reference during comparisons - Added contextual TALKFIRST greetings through both voice-proxy and HTTP reasoning paths - Preserved backend permission checks, conversation isolation, cancellation, and the existing speech protocol Reviewed-on: AquaPortal/s2s#2
84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
"""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
|
|
from vocero_api.services import content_context, 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())
|
|
linked_content = await content_context.from_link(
|
|
login.sessionToken.get_secret_value(), body.report_id, body.episode_id
|
|
)
|
|
browser = request.cookies.get(conversations.COOKIE_NAME) or secrets.token_urlsafe(32)
|
|
item = conversations.store.create(browser, login, linked_content)
|
|
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 content_context.catalog(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
|