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
+9 -6
View File
@@ -29,9 +29,12 @@ ALLOWED_TOOLS = frozenset(
MAX_RESULT_CHARS = 64000
def http_client(token: str) -> httpx.AsyncClient:
def http_client(token: str, podcast_id: str | None = None) -> httpx.AsyncClient:
headers = {"Authorization": f"Bearer {token}"}
if podcast_id is not None:
headers["X-Vocero-Podcast-Id"] = podcast_id
return httpx.AsyncClient(
headers={"Authorization": f"Bearer {token}"},
headers=headers,
timeout=settings.S2S_MCP_TIMEOUT_SEC,
follow_redirects=False,
)
@@ -49,12 +52,12 @@ def _http_status(exc: BaseException) -> int | None:
@asynccontextmanager
async def session(token: str) -> AsyncIterator[ClientSession]:
async def session(token: str, podcast_id: str | None = None) -> AsyncIterator[ClientSession]:
if not settings.S2S_MCP_URL or not token:
raise IntegrationError(401, "Se requiere una sesión válida para consultar AquaPortal.")
try:
async with asyncio.timeout(settings.S2S_MCP_TIMEOUT_SEC):
async with http_client(token) as http:
async with http_client(token, podcast_id) as http:
async with streamable_http_client(settings.S2S_MCP_URL, http_client=http, terminate_on_close=False) as (
reader,
writer,
@@ -99,10 +102,10 @@ async def discover(token: str) -> list[McpToolDefinition]:
raise IntegrationError(502, "El catálogo de AquaPortal no se pudo cargar por completo.")
async def execute(token: str, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
async def execute(token: str, name: str, arguments: dict[str, Any], *, podcast_id: str | None = None) -> dict[str, Any]:
if name not in ALLOWED_TOOLS:
return {"error": "Unknown tool. Choose a tool from the supplied catalog."}
async with session(token) as client:
async with session(token, podcast_id) as client:
result = await client.call_tool(name, arguments)
text_parts = [part.text for part in result.content if part.type == "text"]
if result.isError: