AI voice SDR for outbound sales and qualification

Dialoft

Phone and browser voice are different transports, not different products. One conversation brain serves both, or the two drift apart within a month.

Role
Sole engineer. Conversation graph, tool layer, compliance guards, offline test harness.
Shape
Python, with a LangGraph brain behind a FastAPI service and a separate LiveKit worker.
Links
Source

Architecture

messagesreasontool callsVapiphone channelLiveKitweb channelDeepgramNova/Aura STT+TTS/v1 endpointOpenAI-compatibleLangGraph8-node graphClaudereasoning8 toolsCRM, KB, DNCLead scoreBANT 0-100
VOICE TOPOLOGY

The problem

A voice agent that handles both outbound phone calls and browser sessions usually ends up with two implementations of the same conversation. The telephony vendor has its own assistant configuration, the browser stack has its own agent loop, and the two slowly diverge until a fix applied to one silently misses the other.

Voice also removes the safety net. There is no interface to constrain what the user says, no place to put a disclaimer they will actually read, and every compliance obligation, consent, do-not-call, calling hours, has to be enforced in the conversation itself.

The approach

One LangGraph state machine is the entire conversation brain, and it is exposed behind an OpenAI-compatible chat-completions endpoint. Both the telephony provider and the browser worker are just clients of that endpoint. There is exactly one implementation of discovery, qualification, objection handling, and booking.

Routing is deterministic because the handling stage for each turn is derived from accumulated state rather than chosen by the model. Consent status, filled qualification slots, and lead classification determine the stage, which means the same state always routes the same way and the flow is testable without a model at all.

Objection handling is grounded in a retrieval step over a knowledge base, so the agent answers from source material rather than inventing pricing under pressure.

Decisions

Compliance lives in the graph, not the prompt

Consent disclosure, do-not-call checks, opt-out handling, and calling-window guards are nodes and guards in the state machine. They execute before a dial can happen and cannot be talked around, because they are not instructions the model is asked to follow.

The whole flow verifies with no keys

A deterministic fake model and in-memory CRM, calendar, and do-not-call backends let the entire conversation run offline. 39 tests and a full scripted call execute with no telephony, no speech service, and no API key. This is what makes the agent refactorable: you can change the graph and know within seconds whether qualification still works.

Outbound has a dry-run mode

Without credentials, the outbound endpoint returns exactly the payload it would have sent instead of failing. You can inspect what a real call would do before any real call is possible.

Evidence

1
conversation brain, two voice channels
8
graph nodes, 7 state-derived stages
39
tests, all offline
0
API keys needed to run the suite

What it does not do

The brain and service run on Python 3.9 and above, but the browser worker and speech SDK need 3.10 and above, so they are a separate environment and image.

Lead scoring is a transparent rule-based function over the qualification slots rather than a learned model, which is the right trade at this data volume but will not capture subtler signals.

Other systems