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
+40 -11
View File
@@ -131,13 +131,14 @@ not returned to the browser. Dynamic ApiPilot tools are configured through
Operator RAG text is applied through the RAG dialog and updates live
instructions. Caller links may include `phoneNumber=%2B...`; episode links may
include `episode_id=...`. See [context injection](context-injection.md) for
include `episode_id=...`; MCP report links may include `report_id=...`.
See [context injection](context-injection.md) for
enabled data sources and limits.
### Authenticated AquaPortal MCP
`S2S_MCP_URL` enables the MCP client transport. Set it to the backend's complete
Streamable HTTP endpoint, for example `https://backend.example.test/api/v1/mcp`.
Streamable HTTP endpoint, for example `https://backend.example.test/api/v1/mcp/`.
Set `S2S_LOGIN_BY_PHONE` to that backend's `POST /api/v1/auth/phone-login` URL.
The phone parameter remains an identity selector without proof of ownership.
MCP uses the returned opaque `sessionToken`, never an ApiPilot API key or service token.
@@ -165,7 +166,8 @@ and caller HTTP routes return 409 while MCP is enabled.
| `S2S_CONVERSATION_TTL_SEC` | `3600` | Absolute conversation lifetime, clamped to 60–86400 seconds |
| `S2S_MAX_CONVERSATIONS` | `1000` | Process-local conversation capacity, clamped to 1–10000 |
`POST /api/conversations` accepts `{"phoneNumber":"+56000000000"}` and returns
`POST /api/conversations` accepts `{"phoneNumber":"+56000000000"}` with optional
`report_id` and `episode_id` fields, and returns
`conversationId`, `expiresIn`, and a minimal caller profile. An HttpOnly,
SameSite=Strict cookie binds the browser to its conversations; HTTPS sets Secure.
Backend credentials stay in memory on the API server. HTTP conversation requests
@@ -191,12 +193,29 @@ reasoning retains up to six completed turns, bounded to 96,000 characters,
within that conversation; the speech service keeps its existing conversation
history. These histories are cleared when their sessions end.
The browser waits for successful phone login and discovery before starting an
MCP conversation. Failure blocks startup and displays a retryable error. Phone
and `episode_id` page parameters remain supported, but automatic selection of
the episode from the link is deferred: resolve it explicitly with the podcast
tools for now. Changing browser-supplied profile, phone, or episode values on a
reasoning request cannot change the stored backend identity.
The browser waits for phone login, linked-content resolution, and tool discovery
before starting an MCP conversation. Login or discovery failure blocks startup
and displays a retryable error. Link resolution is limited to ten seconds and
uses only authenticated MCP tools. Missing, inaccessible, or temporarily
unavailable linked content permits a generic conversation; an expired backend
session still prevents startup. The browser allows thirty seconds for login
and link resolution together.
For example, `?phoneNumber=%2B56000000000&report_id=report-example` selects the
report and its podcast, `report:report-example`, when available. The full report
ID is preserved. An `episode_id` selects a daily podcast, `daily:<episode_id>`.
When both are present, the explicit daily episode determines the podcast and
`report_id` remains a separate report reference. No unavailable episode is
silently replaced with a different one.
The link provides the initial subject, not a search filter or an access grant.
Users can ask about any accessible report, compare multiple reports, or choose
another subject. The local `select_content` action validates that choice and
updates the current reference while preserving the original link for follow-ups.
Merely searching or reading content does not change the selection. A new
conversation, including after restart, resolves the page's selectors again.
Changing browser-supplied profile, phone, report, or episode values on a
reasoning request cannot change the stored identity or selection.
## Theme and session startup
@@ -213,7 +232,7 @@ gets an eight-second loading window before the interface allows a generic sessio
|---|---|
| Empty/default | User taps to start |
| `TALK` | Attempt automatic start and inject the Spanish wake phrase as soon as configuration and caller context are ready |
| `TALKFIRST` | Same automatic startup behavior as `TALK` |
| `TALKFIRST` | Same startup trigger as `TALK`; with MCP, greet using the linked report or podcast title after authenticated resolution |
Automatic startup runs once per page load, without a startup dialog or artificial
delay; optional reports and tools can still be loading. Browser permission and
@@ -221,7 +240,17 @@ audio policies can still require a gesture. When audio is blocked, an inline hin
asks the user to tap the microphone button. Permission or connection failures
show the error and leave the same button available to retry. Stopping a session
does not start it again automatically.
The greeting uses the same `hola vocero` input as wake-word handling.
The greeting uses the same `hola vocero` input as wake-word handling. With MCP
and TALKFIRST, the greeting names the linked report, or the podcast when no
report is available, and invites questions about this or other reports. It does
not summarize automatically. A missing reference gets a brief explanation;
links without a content selector receive a generic greeting.
In `PROXY`, the relay supplies the verified metadata in the initial speech
instructions. In `OPENAI`, the controller sends the opening input through the
authenticated HTTP reasoning path after the speech connection is ready, then
uses the existing external-answer playback. Speaking first or stopping the
session cancels a pending greeting. Neither path changes the speech service.
On browsers with speech recognition, **Activar con «hola vocero»** opts into
wake-word listening for the current page session. The interface shows when the