Skip to content

Add a VAPI adapter: webhook handler in, control-URL nudges out - #3

Open
amanmibra wants to merge 2 commits into
mainfrom
devin/1789239850-vapi-adapter
Open

amanmibra wants to merge 2 commits into
mainfrom
devin/1789239850-vapi-adapter

Conversation

@amanmibra

@amanmibra amanmibra commented Sep 12, 2026

Copy link
Copy Markdown
Member

Summary

deeptrust.agents.vapi.Bridge — the third adapter, for a customer on VAPI (inbound restaurant calls). Unlike the other two, nothing here holds a connection: VAPI POSTs server messages to the customer's webhook, and the customer calls bridge.handle(payload, user=...) from that route.

bridge = Bridge(DeepTrust(), api_key=os.environ["VAPI_API_KEY"])
await bridge.handle(payload)   # inside POST /vapi/webhook

Per message (payload or payload["message"], keyed on call.id, platform="vapi"):

  • transcript with transcriptType == "final" (or the filtered type name transcript[transcriptType="final"]) → session.append(role, text); analyse on user turns only. Partials are dropped — the same guard against re-analysing one sentence that livekit.py's last dict provides.
  • end-of-call-reportsession.end(), and the call is forgotten.
  • everything else (speech/status/conversation updates, tool-calls…) → ignored, so the whole server URL can be routed here.

Nudge delivery is POST {controlUrl} with

{"type": "add-message", "message": {"role": "system", "content": nudge.render()}, "triggerResponseEnabled": true}

which makes the agent respond now — LiveKit-interrupt semantics rather than ElevenLabs' next-turn contextual update. Module docstring and README say so.

controlUrl is taken from call.monitor.controlUrl when the webhook carries it and it is https on a *.vapi.ai host (the body is untrusted input, so a URL pointing elsewhere is dropped), else fetched once per call via GET https://api.vapi.ai/call/{id} with the VAPI key (under a per-call lock), so inbound calls never created by the customer's code work. listenUrl is not touched. Verifying the webhook's origin (x-vapi-secret) is the customer's route's job and is documented as such. Session.check untouched.

No new dependency (httpx is core), so no vapi extra; vapi added to keywords and to the mypy adapter override. aclose closes the httpx client only if the bridge created it. Tests in tests/test_vapi.py are respx-only: payload sequence in, analyze calls and control-URL body out, plus lookup/delivery failure paths.

The justfile check target passes (ruff, mypy, 38 tests) locally.

Link to Devin session: https://app.devin.ai/sessions/04b304333cb047a49ca2cfd938449671
Open in Devin Desktop: https://app.devin.ai/desktop/session/04b304333cb047a49ca2cfd938449671?variant=devin
Requested by: @amanmibra


Devin Review

Co-Authored-By: Aman Ibrahim <aman@deeptrust.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Original prompt from Aman Ibrahim

Add a VAPI adapter to the Python SDK: new file src/deeptrust/agents/vapi.py, exported the way livekit and elevenlabs are.

IMPORTANT — do not copy the ElevenLabs Monitor class shape. The transport is fundamentally different:

  • ElevenLabs: we open a websocket and hold it. VAPI: VAPI POSTs webhooks to the CUSTOMER's server, and we push nudges back out over a per-call HTTPS control URL. We are never the one holding a connection.
  • So the adapter must expose (a) a handler the customer calls from inside their own webhook route, and (b) nudge delivery that POSTs to that call's controlUrl.

Proposed shape (adjust if the codebase suggests better):
from deeptrust.agents.vapi import Bridge
bridge = Bridge(DeepTrust(), api_key=os.environ['VAPI_API_KEY'])
# inside the customer's POST /vapi/webhook route:
await bridge.handle(payload, user=user)

Behaviour:

  1. Read turns from the 'transcript' server message. Use ONLY final transcripts (there is a partial/final distinction) — analysing partials re-analyses the same sentence repeatedly, which is the bug the LiveKit adapter's 'last' dict already guards against. Mirror that intent.
  2. Append with call.append(role, text), analyse on user turns only (same rule as both existing adapters — never analyse the agent's own replies).
  3. Deliver nudges to the call's controlUrl as:
    {"type": "add-message", "message": {"role": "system", "content": nudge.render()}, "triggerResponseEnabled": true}
    triggerResponseEnabled:true makes the agent respond immediately, so VAPI nudges behave like LiveKit's interrupt rather than ElevenLabs' next-turn contextual update. Document that difference in the module docstring — the existing two modules both document their own delivery semantics and this one should match that standard.
  4. external_id is VAPI's call id. platform='vapi'.
  5. Resolve controlUrl: it arrives on the call object as monitor.controlUrl. Accept it from the webhook payload when present, otherwise GET /call/{id} with the VAPI... (2612 chars truncated...)

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

3 bugs not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment thread tests/test_vapi.py
Comment thread src/deeptrust/agents/vapi.py Outdated
Comment thread src/deeptrust/agents/vapi.py Outdated
Comment thread src/deeptrust/agents/vapi.py
…ur own client; error-path tests

Co-Authored-By: Aman Ibrahim <aman@deeptrust.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant