feat(vocero): add linked content and TALKFIRST greetings (#2)

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
This commit is contained in:
2026-10-08 09:34:16 -04:00
parent 96895996d9
commit 4b0108ecf0
21 changed files with 946 additions and 66 deletions
@@ -6,8 +6,8 @@ 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
from vocero_api.providers import backend_login
from vocero_api.services import content_context, conversations
router = APIRouter()
@@ -23,8 +23,11 @@ async def create_conversation(body: ConversationRequest, request: Request, respo
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)
item = conversations.store.create(browser, login, linked_content)
except IntegrationError as exc:
raise HTTPException(exc.status, exc.message) from None
response.set_cookie(
@@ -69,7 +72,7 @@ async def list_tools(request: Request, response: Response) -> McpCatalogResponse
item: conversations.Conversation | None = None
try:
item = conversations.resolve(request)
catalog = await mcp.discover(item.backend_token)
catalog = await content_context.catalog(item.backend_token)
item.require_active()
item.catalog = catalog
response.headers["Cache-Control"] = "no-store"