DTMF Handling
DTMF (Dual-Tone Multi-Frequency) allows users to interact using their phone keypad, essential for secure input and menu navigation.
What is DTMF?
Phone Keypad:
┌─────┬─────┬─────┐
│ 1 │ 2 │ 3 │
│ │ ABC │ DEF │
├─────┼─────┼─────┤
│ 4 │ 5 │ 6 │
│ GHI │ JKL │ MNO │
├─────┼─────┼─────┤
│ 7 │ 8 │ 9 │
│PQRS │ TUV │WXYZ │
├─────┼─────┼─────┤
│ * │ 0 │ # │
│ │ │ │
└─────┴─────┴─────┘
Each key generates a unique tone pair
that can be detected and processed.
Configuration
Enable DTMF
{
"agent": {
"name": "IVR Agent",
"dtmf": {
"enabled": true,
"interDigitTimeout": 3000,
"maxDigits": 20,
"terminators": ["#"]
}
}
}
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true | Enable DTMF detection |
interDigitTimeout |
int | 3000 | Max ms between digits |
maxDigits |
int | 20 | Maximum digits to collect |
terminators |
array | ["#"] | Keys that end input |
minDigits |
int | 1 | Minimum digits required |
Implementation
DTMF Handler
type DTMFHandler struct {
buffer strings.Builder
lastDigit time.Time
timeout time.Duration
maxDigits int
minDigits int
terminators []string
callback func(digits string)
}
func (h *DTMFHandler) OnDigit(digit string) {
h.lastDigit = time.Now()
h.buffer.WriteString(digit)
// Check for terminator
for _, term := range h.terminators {
if digit == term {
h.complete()
return
}
}
// Check max digits
if h.buffer.Len() >= h.maxDigits {
h.complete()
return
}
// Start/reset timeout timer
h.startTimeout()
}
func (h *DTMFHandler) startTimeout() {
go func() {
time.Sleep(h.timeout)
if time.Since(h.lastDigit) >= h.timeout {
if h.buffer.Len() >= h.minDigits {
h.complete()
}
}
}()
}
func (h *DTMFHandler) complete() {
digits := h.buffer.String()
// Remove terminator if present
digits = strings.TrimSuffix(digits, "#")
digits = strings.TrimSuffix(digits, "*")
h.buffer.Reset()
h.callback(digits)
}
Integration with Pipeline
func (p *Pipeline) handleDTMF(digit string) {
// Pause STT during DTMF collection
if p.collectingDTMF {
p.dtmfHandler.OnDigit(digit)
return
}
// Single digit actions
switch digit {
case "0":
p.transferToOperator()
case "*":
p.repeatLastMessage()
default:
// Treat as start of DTMF sequence
p.startDTMFCollection(digit)
}
}
Common Use Cases
Menu Navigation
systemPrompt := `You are an IVR assistant.
When presenting options, always say:
"Press 1 for [option], Press 2 for [option]..."
Available menus:
- Main: 1=Orders, 2=Support, 3=Billing, 0=Operator
- Orders: 1=Status, 2=Returns, 3=New Order
- Support: 1=Technical, 2=Account, 3=Other`
func (p *Pipeline) handleMenuDTMF(digit string) {
menu := p.currentMenu
switch menu {
case "main":
switch digit {
case "1":
p.setMenu("orders")
p.tts.Speak("Orders menu. Press 1 for order status...")
case "2":
p.setMenu("support")
p.tts.Speak("Support menu. Press 1 for technical support...")
case "0":
p.transferToOperator()
}
case "orders":
// Handle orders submenu
}
}
PIN/Password Entry
func (p *Pipeline) collectPIN() string {
p.tts.Speak("Please enter your 4-digit PIN, followed by the pound key.")
// Pause recording for security
p.recorder.Pause()
defer p.recorder.Resume()
// Mute STT to avoid PIN in transcript
p.stt.Mute()
defer p.stt.Unmute()
// Collect DTMF
p.dtmfHandler.Configure(DTMFConfig{
MinDigits: 4,
MaxDigits: 4,
Terminators: []string{"#"},
Timeout: 10 * time.Second,
})
pin := <-p.dtmfHandler.Result()
return pin
}
Phone Number Collection
func (p *Pipeline) collectPhoneNumber() string {
p.tts.Speak("Please enter your 10-digit phone number, followed by the pound key.")
p.dtmfHandler.Configure(DTMFConfig{
MinDigits: 10,
MaxDigits: 11, // Allow country code
Terminators: []string{"#"},
Timeout: 15 * time.Second,
})
phone := <-p.dtmfHandler.Result()
// Validate
if len(phone) < 10 {
p.tts.Speak("I didn't get a complete phone number. Let's try again.")
return p.collectPhoneNumber()
}
// Format and confirm
formatted := formatPhoneNumber(phone)
p.tts.Speak(fmt.Sprintf("I have %s. Is that correct? Press 1 for yes, 2 for no.",
speakPhoneNumber(formatted)))
confirmation := <-p.dtmfHandler.Result()
if confirmation == "1" {
return formatted
}
return p.collectPhoneNumber()
}
func speakPhoneNumber(phone string) string {
// "4 1 5. 5 5 5. 1 2 3 4"
var parts []string
for i, digit := range phone {
if i == 3 || i == 6 {
parts = append(parts, ".")
}
parts = append(parts, string(digit))
}
return strings.Join(parts, " ")
}
Credit Card Entry (PCI Compliant)
func (p *Pipeline) collectCardNumber() string {
p.tts.Speak("For security, please enter your 16-digit card number using your keypad, followed by pound.")
// Disable recording and transcription
p.recorder.Pause()
p.stt.Mute()
p.dtmfHandler.Configure(DTMFConfig{
MinDigits: 15, // Some cards have 15 digits
MaxDigits: 16,
Terminators: []string{"#"},
Timeout: 30 * time.Second,
MaskInput: true, // Don't log digits
})
cardNumber := <-p.dtmfHandler.Result()
// Re-enable
p.recorder.Resume()
p.stt.Unmute()
// Validate using Luhn algorithm
if !isValidCardNumber(cardNumber) {
p.tts.Speak("That doesn't appear to be a valid card number. Let's try again.")
return p.collectCardNumber()
}
// Confirm last 4 digits only
last4 := cardNumber[len(cardNumber)-4:]
p.tts.Speak(fmt.Sprintf("Card ending in %s. Is that correct? Press 1 for yes, 2 for no.",
speakDigits(last4)))
if <-p.dtmfHandler.Result() == "1" {
return cardNumber
}
return p.collectCardNumber()
}
func isValidCardNumber(number string) bool {
// Luhn algorithm
sum := 0
alternate := false
for i := len(number) - 1; i >= 0; i-- {
n := int(number[i] - '0')
if alternate {
n *= 2
if n > 9 {
n -= 9
}
}
sum += n
alternate = !alternate
}
return sum%10 == 0
}
Provider Integration
Twilio
func (h *TwilioHandler) handleMediaMessage(msg TwilioMediaMessage) {
if msg.Event == "dtmf" {
digit := msg.Dtmf.Digit
h.pipeline.HandleDTMF(digit)
}
}
Plivo
func (h *PlivoHandler) handleControlMessage(msg PlivoControlMessage) {
if msg.Event == "dtmf" {
digit := msg.Digit
h.pipeline.HandleDTMF(digit)
}
}
Exotel
func (h *ExotelHandler) handleDTMF(digit string) {
h.pipeline.HandleDTMF(digit)
}
LLM Integration
Let the LLM handle DTMF naturally:
systemPrompt := `You are a voice assistant with DTMF capability.
When you need numeric input (phone numbers, PINs, account numbers),
ask the user to enter it on their keypad.
Example:
User: "I want to check my account balance"
You: "I can help with that. Please enter your 6-digit account number on your keypad, followed by the pound key."
After receiving DTMF input, you'll get a message like:
[DTMF Input: 123456]
Then continue the conversation naturally.`
func (p *Pipeline) handleDTMFComplete(digits string) {
// Inject as system message
p.context.AddMessage(Message{
Role: "system",
Content: fmt.Sprintf("[DTMF Input: %s]", digits),
})
// Continue LLM conversation
p.processWithLLM()
}
Error Handling
func (h *DTMFHandler) handleTimeout() {
if h.buffer.Len() == 0 {
h.callback("timeout:no_input")
} else if h.buffer.Len() < h.minDigits {
h.callback("timeout:incomplete:" + h.buffer.String())
} else {
h.complete()
}
}
func (p *Pipeline) handleDTMFResult(result string) {
if strings.HasPrefix(result, "timeout:") {
if result == "timeout:no_input" {
p.tts.Speak("I didn't receive any input. Let me repeat that.")
p.repeatLastPrompt()
} else {
p.tts.Speak("I didn't get enough digits. Please try again.")
p.retryDTMFCollection()
}
return
}
// Valid input
p.processDTMFInput(result)
}
Best Practices
1. Clear Instructions
// Good
"Please enter your 10-digit phone number, followed by the pound sign."
// Bad
"Enter your phone number."
2. Confirmation for Important Data
func confirmInput(input, inputType string) bool {
message := fmt.Sprintf("You entered %s. Press 1 to confirm, or 2 to re-enter.",
formatForSpeech(input, inputType))
tts.Speak(message)
confirmation := waitForDTMF(1)
return confirmation == "1"
}
3. Timeout Handling
func (p *Pipeline) collectWithRetry(prompt string, config DTMFConfig) (string, error) {
maxRetries := 3
for attempt := 0; attempt < maxRetries; attempt++ {
p.tts.Speak(prompt)
result := p.collectDTMF(config)
if !strings.HasPrefix(result, "timeout:") {
return result, nil
}
if attempt < maxRetries-1 {
p.tts.Speak("I didn't catch that. Let me try again.")
}
}
return "", fmt.Errorf("DTMF collection failed after %d attempts", maxRetries)
}
Next Steps
- Recording - Pause during sensitive DTMF
- Call Transfer - Transfer on "0"
- Security - PCI compliance