Vocero needs to query AquaPortal on behalf of the caller selected by the existing phone link. This adds server-owned conversations and MCP tool execution for voice and HTTP reasoning while leaving the speech pipeline unchanged. Release notes: - Added browser-bound conversations that keep backend credentials server-side and isolate callers. - Connected the eight-tool AquaPortal catalog through authenticated Streamable HTTP with full input schemas and structured results. - Added bounded multi-step tool execution with cancellation, expiry, and stale-result protection. - Updated browser bootstrap, tool progress, reasoning headers, and conversation cleanup. - Disabled legacy unscoped content preloads in MCP mode and documented configuration and deployment constraints. - Preserved disabled-mode contracts and added local SDK, WebSocket, and browser regression coverage. Reviewed-on: AquaPortal/s2s#1
205 lines
9.2 KiB
Markdown
205 lines
9.2 KiB
Markdown
# Tool calling
|
|
|
|
The application supports native realtime function calling and server-side
|
|
tool selection from model text. AquaPortal content uses authenticated MCP when
|
|
`S2S_MCP_URL` is set; otherwise the legacy ApiPilot bridge remains available.
|
|
A spoken promise to fetch something is not evidence that a tool executed.
|
|
|
|
## Authenticated AquaPortal MCP
|
|
|
|
Vocero uses the official MCP Python SDK with Streamable HTTP. Each discovery
|
|
or execution initializes a client with that conversation's opaque backend token
|
|
as `Authorization: Bearer ...`. JSON and SSE responses are supported. Neither
|
|
browser values nor model arguments choose the authenticated user. There is no
|
|
service-token override, anonymous retry, redirect following, or public REST
|
|
fallback on this path. AquaPortal checks current permissions on every request,
|
|
including its administrator and assigned report-manager access rules.
|
|
|
|
The backend phone login must return `sessionToken` plus `user.id` and `user.name`.
|
|
The only allowed tool names are:
|
|
|
|
| Tool | Content |
|
|
|---|---|
|
|
| `list_reports` | Accessible published reports with name, center, and date filters |
|
|
| `get_report` | One report or an explicit list of ambiguous selections |
|
|
| `list_insights` | Published findings, priorities, recommendations, and sources |
|
|
| `get_metric_history` | Dated metric values, units, centers, cages, and source reports |
|
|
| `list_podcasts` | Daily and report-associated episodes |
|
|
| `get_podcast` | Selected episode summary, highlights, and available media |
|
|
| `read_podcast_transcript` | Paginated transcript or report-script passages |
|
|
| `get_my_briefing` | Compact overview with actual source dates |
|
|
|
|
Discovery preserves the complete input schema, including nested `request`,
|
|
optional filters, references, and pagination. The executor validates arguments
|
|
against that schema and supplies `structuredContent` to reasoning when present,
|
|
falling back to MCP text content. It preserves `get_report`'s `result` union and
|
|
list `next_offset` values. Tool errors are data for the model to explain; expired
|
|
authentication stops the conversation. Tool text is labeled as reference data,
|
|
never as instructions or authorization.
|
|
|
|
Both reasoning paths can discover reports, retrieve details, and follow up with
|
|
another tool before answering. The HTTP loop retains bounded completed-turn
|
|
history. The realtime relay injects results into the speech service's existing
|
|
conversation. Attempts are bounded per user turn (six by default), identical
|
|
calls within a turn are rejected, and tool results are limited to 64,000
|
|
characters individually and 96,000 per turn. Each MCP operation has a timeout;
|
|
HTTP reasoning also has a 180-second total timeout.
|
|
|
|
Selection uses an internal text marker, not a new speech protocol event:
|
|
|
|
```text
|
|
<<<VOCERO_MCP_CALL:NAME=get_report,ARGUMENTS={"request":{"report_id":"example-id"}}>>>
|
|
```
|
|
|
|
The distinct marker is necessary because the current speech pipeline removes
|
|
`APIPILOT_CALL` before emitting assistant text. The relay buffers undecided
|
|
text/audio, handles transcripts emitted before `response.created`, executes
|
|
the MCP selection, and requests another response. Normal speech continues using
|
|
the existing PCM16 protocol. This leaves `speech_to_speech/` unchanged, although
|
|
the speech service may spend time synthesizing audio that the relay suppresses.
|
|
Malformed selections fail with a user-facing error rather than permissive parsing.
|
|
|
|
New speech, cancellation, disconnect, and expiry invalidate pending work.
|
|
Generation checks reject late results even if the transport does not stop in
|
|
time. The browser receives `vocero.tool.started` / `vocero.tool.failed` status
|
|
events, without backend credentials or raw tool arguments.
|
|
|
|
Automatic current-episode context from `episode_id` is not wired yet. The model
|
|
must resolve a podcast with the catalog rather than fetching an unauthenticated
|
|
episode URL. See [usage](usage.md#authenticated-aquaportal-mcp) for configuration,
|
|
conversation endpoints, single-worker constraints, and the retained unsafe
|
|
phone selector.
|
|
|
|
## Native realtime function calls
|
|
|
|
The browser sends tool declarations inside the `session` object:
|
|
|
|
```json
|
|
{
|
|
"type": "session.update",
|
|
"session": {
|
|
"type": "realtime",
|
|
"tools": [
|
|
{
|
|
"type": "function",
|
|
"name": "web_search",
|
|
"description": "Search the web for information.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string"}},
|
|
"required": ["query"]
|
|
}
|
|
}
|
|
],
|
|
"tool_choice": "auto"
|
|
}
|
|
}
|
|
```
|
|
|
|
Keep configuration inside `session`, not at the event root. The backend has a
|
|
legacy flat-payload compatibility path, but new clients should use the nested
|
|
shape. The current browser's `activeToolDefs()` declares `web_search` when
|
|
enabled and a deploy/user search key is available. It does not load the old
|
|
remote agent-profile MCP catalog into native session tools.
|
|
|
|
For a native call, the backend emits:
|
|
|
|
```json
|
|
{
|
|
"type": "response.function_call_arguments.done",
|
|
"call_id": "call_example",
|
|
"name": "web_search",
|
|
"arguments": "{\"query\":\"example query\"}"
|
|
}
|
|
```
|
|
|
|
`arguments` is a JSON-encoded string. The
|
|
[WebSocket client](../../vocero-frontend/src/realtime/client.ts) validates the event and
|
|
dispatches `tool`; [controller.ts](../../vocero-frontend/src/realtime/controller.ts)
|
|
parses the arguments and runs the supported executor.
|
|
Web search uses `POST /api/search`, keeping the deployment's Serper key on the
|
|
server. Tool output returns to the model as:
|
|
|
|
```json
|
|
{
|
|
"type": "conversation.item.create",
|
|
"item": {
|
|
"type": "function_call_output",
|
|
"call_id": "call_example",
|
|
"output": "Tool result text"
|
|
}
|
|
}
|
|
```
|
|
|
|
The client requests follow-up generation with `response.create` when the
|
|
result should be spoken. A `function_call_output` item alone does not start
|
|
generation. See the [realtime API guide](../speech/realtime-api.md) for local
|
|
model `<code>` extraction and native remote-provider tool calls.
|
|
|
|
## ApiPilot discovery and execution
|
|
|
|
When MCP is disabled, `S2S_APIPILOT_TOOLS_URL` enables the legacy dynamic-tool bridge. The backend
|
|
discovers tools through its `/tools` route and executes through `/execute`.
|
|
`S2S_APIPILOT_TOOLS_EXECUTE_URL` can override the execution target. The same-origin UI
|
|
server exposes:
|
|
|
|
- `GET /api/apipilot/tools?include_unauthorized=true`: discovery envelope with
|
|
`data.tools`, `data.count`, and `data.include_unauthorized`.
|
|
- `POST /api/apipilot/execute`: body `{"name":"tool_name","arguments":{}}` and
|
|
a `success/data/error` envelope.
|
|
|
|
Tool definitions contain `name`, `description`, JSON Schema `parameters`, and
|
|
`requires_auth`. Execution looks up the current discovery cache, refreshes once
|
|
if the tool is missing, and applies an authorization gate and argument
|
|
validation before forwarding. Caller `Authorization` and
|
|
`x-demo-session` can be forwarded; configured service credentials and
|
|
login-by-phone provide additional server paths. Display `error.message` to the
|
|
user; keep `error.code`, `error.status_code`, and `error.raw` for diagnostics.
|
|
|
|
The discovery cache defaults to 300 seconds server-side
|
|
(`S2S_APIPILOT_TTL_SEC`) and can be refreshed with `refresh=true`. The browser
|
|
also keeps a per-user catalog cache. ApiPilot configuration is independent of
|
|
`S2S_INJECT_PROFILE`.
|
|
|
|
## Marker-based two-pass reasoning
|
|
|
|
The browser and proxy add an ApiPilot catalog as **instruction text**, including
|
|
tool names, descriptions, authentication requirements and required parameters.
|
|
The model is instructed to emit a call marker when it chooses a matching tool:
|
|
|
|
```text
|
|
<<<APIPILOT_CALL:NAME=tool_name,ARGUMENTS={"parameter":"value"}>>>
|
|
```
|
|
|
|
Tool names are exact identifiers and must not be translated. The local realtime
|
|
proxy accumulates assistant text, detects the marker, validates and executes
|
|
the ApiPilot call, then injects the result as context and requests a new
|
|
response. The HTTP reasoning route has analogous discovery/execution and
|
|
second-answer handling. Internal marker text and its speculative audio are
|
|
filtered from user output where those handlers run.
|
|
|
|
This marker is not `response.function_call_arguments.done`, and ApiPilot prompt
|
|
discovery is not the same as declaring a native `session.tools` array. Direct
|
|
browser connections bypass the local relay's marker executor even if the
|
|
browser includes catalog instructions.
|
|
|
|
## Retired documentation assumptions
|
|
|
|
The former `HOWITWORKS.md` described `/api/mcp/tool`, remote
|
|
`S2S_AGENT_PROFILE` loading, and Spanish/Italian-to-English client ranking.
|
|
Those APIs/rankers are absent from the current client/server path. The retained
|
|
agent-profile JSON is an illustrative historical/example shape. Add or modify
|
|
current ApiPilot tool definitions in the provider; do not restore obsolete
|
|
endpoints solely to match older documentation.
|
|
|
|
## Verify a tool turn
|
|
|
|
1. Confirm `/api/config` reports the intended feature and connection mode.
|
|
2. Inspect `session.update`: native `tools` and ApiPilot instruction text are
|
|
separate evidence.
|
|
3. For native calls, find `response.function_call_arguments.done`, then the
|
|
browser `toolcall` and executor request.
|
|
4. For ApiPilot, find the discovery, call-marker detection and execution logs.
|
|
5. Confirm the tool result reaches the model and follow-up text/audio reaches
|
|
the browser. A spoken acknowledgement alone does not complete the flow.
|