# AMDY.IO Asterisk AMD Installation Skill

You are an expert at installing AMDY.IO AI answering machine detection on Asterisk-based dialer servers. Follow this skill to help users install the EAGI AMD script, register their server IP, and integrate it into their dialplan.

## When to Use

Trigger this skill when a user wants to:
- Install AMD / answering machine detection on an Asterisk server
- Set up AMDY.IO on a custom dialer (not ViciDial/Vicibox)
- Add AI-powered AMD to their PBX or dialer
- Fix or troubleshoot an existing AMDY EAGI installation

## Prerequisites (verify before starting)

1. **Root/sudo access** to the Asterisk server
2. **Asterisk installed** (`asterisk -V` should return a version)
3. **AMDY.IO API key** — user gets this from https://app.amdy.io/settings
4. **Outbound internet** to `ws://api.amdy.io:2700` and `https://app.amdy.io`

## Installation Steps

### Step 1: Get the API key

Ask the user for their AMDY.IO API key. They can find it at:
- https://app.amdy.io/settings → API Keys section

### Step 2: Run the installer

```bash
curl -sL https://download.amdy.io/installamd-asterisk.sh | bash -s -- YOUR_API_KEY
```

Or with explicit flag:
```bash
bash installamd-asterisk.sh --api-key YOUR_API_KEY
```

The installer will:
- Install Python 3 + pip (if missing)
- Install `pyst2` and `websocket-client` Python packages
- Download the EAGI script to `/var/lib/asterisk/agi-bin/amdy-amd.py`
- Store the API key in `/etc/amdy/api-key` (mode 600, asterisk-owned)
- Register the server's public IP with the AMDY.IO account
- Print dialplan instructions

### Step 3: Find where to insert EAGI in the dialplan

Before adding AMDY, find the context in the dialplan where outbound calls are placed. The EAGI script must be inserted **after Answer() and before routing to an agent**.

**Search for the outbound dialing context:**

```bash
# Find where calls are dialed out
grep -rn "Dial(" /etc/asterisk/extensions.conf
grep -rn "Dial(" /etc/asterisk/extensions*.conf
# If using a database-backed dialplan:
asterisk -rx "dialplan show" | grep -i "dial\|outbound\|trunk"
```

**Common context names for outbound calls:**
- `[outbound]`, `[outbound-calls]`, `[trunk-out]`
- `[from-internal]`, `[local-calls]`, `[longdistance]`
- `[macro-dialout]`, `[sub-dialout]` (in macro/subroutine form)
- Custom names like `[sales-outbound]`, `[campaign-1]`

**Identify the call flow.** The outbound context typically looks like:

```asterisk
[outbound-calls]
exten => _1NXXNXXXXXX,1,Set(CALLERID(num)=${OUTBOUND_CID})
exten => _1NXXNXXXXXX,n,Dial(SIP/${EXTEN}@your-trunk,30)
exten => _1NXXNXXXXXX,n,Hangup()
```

**Insert EAGI after Answer() and before routing:**

```asterisk
[outbound-calls]
exten => _1NXXNXXXXXX,1,Set(CALLERID(num)=${OUTBOUND_CID})
exten => _1NXXNXXXXXX,n,Dial(SIP/${EXTEN}@your-trunk,30)
; ↓↓↓ INSERT AMDY AMD HERE — after the far end answers ↓↓↓
exten => _1NXXNXXXXXX,n,Answer()
exten => _1NXXNXXXXXX,n,EAGI(amdy-amd.py)
; ↓↓↓ THEN ROUTE BASED ON RESULT ↓↓↓
exten => _1NXXNXXXXXX,n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?human:machine)
exten => _1NXXNXXXXXX,n(machine),NoOp(AMD: ${AMDCAUSE})
exten => _1NXXNXXXXXX,n(machine),Hangup()
exten => _1NXXNXXXXXX,n(human),Queue(sales-queue)
```

**Important:** If the dialplan uses `Dial()` with the `U` or `b` option (pre-connect subroutine), insert EAGI inside that subroutine. If it uses `Gosub` or `Macro` after answer, insert EAGI at the start of that subroutine.

### Step 4: IF-based routing on return values

After `EAGI(amdy-amd.py)` runs, three channel variables are set. Use `GotoIf` to route the call:

#### Basic: Human vs Machine

```asterisk
exten => s,n,EAGI(amdy-amd.py)

; Simple binary routing
exten => s,n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?human:machine)

exten => s,n(machine),NoOp(Machine detected: ${AMDCAUSE})
exten => s,n,Hangup()

exten => s,n(human),NoOp(Human detected — routing to agent)
exten => s,n,Queue(sales-queue)
```

#### Advanced: Route by Machine Type (AMDCAUSE)

```asterisk
exten => s,n,EAGI(amdy-amd.py)

; Human → agent
exten => s,n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?human)

; Voicemail → drop
exten => s,n,GotoIf($["${AMDCAUSE}" = "VOICEMAIL"]?voicemail)
exten => s,n,GotoIf($["${AMDCAUSE}" = "CUSTOMVMAMD"]?voicemail)
exten => s,n,GotoIf($["${AMDCAUSE}" = "WELCOMEVMAMD"]?voicemail)

; Fax → skip
exten => s,n,GotoIf($["${AMDCAUSE}" = "FAX"]?fax)

; Honeypot / spam trap → flag and skip
exten => s,n,GotoIf($["${AMDCAUSE}" = "HONEYPOTAMD"]?honeypot)

; FAS (carrier false answer) → retry
exten => s,n,GotoIf($["${AMDCAUSE}" = "FASAMD"]?fas)

; Default machine → hangup
exten => s,n,Hangup()

exten => s,n(human),Queue(sales-queue)
exten => s,n(voicemail),NoOp(Voicemail — skipping)
exten => s,n,Hangup()
exten => s,n(fax),NoOp(Fax machine — skipping)
exten => s,n,Hangup()
exten => s,n(honeypot),NoOp(HONEYPOT — flagging and skipping)
exten => s,n,Set(CDR(userfield)=HONEYPOT:${CALLERID(num)})
exten => s,n,Hangup()
exten => s,n(fas),NoOp(Carrier FAS — will retry)
exten => s,n,Hangup()
```

#### Predictive Dialer: Connect Only Humans to Agents

```asterisk
exten => s,n,EAGI(amdy-amd.py)

; If machine, hangup immediately (don't waste agent time)
exten => s,n,GotoIf($["${AMDSTATUS}" != "HUMAN"]?hangup)

; Human detected — bridge to available agent
exten => s,n,Set(CHANNEL(musicclass)=default)
exten => s,n,Queue(predictive-queue,tTkK,,,30)
exten => s,n(hangup),NoOp(Non-human: ${AMDCAUSE} — releasing line)
exten => s,n,Hangup()
```

#### IVR: Play Message Only to Humans

```asterisk
exten => s,n,EAGI(amdy-amd.py)
exten => s,n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?play:skip)

exten => s,n(skip),Hangup()

exten => s,n(play),Playback(welcome-message)
exten => s,n,Background(main-menu)
```

#### Log All Detections to CDR

```asterisk
exten => s,n,EAGI(amdy-amd.py)
exten => s,n,Set(CDR(userfield)=AMD:${AMDSTATUS}:${AMDCAUSE})
exten => s,n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?connect:skip)
exten => s,n(connect),Queue(sales)
exten => s,n(skip),Hangup()
```

### Step 5: Reload and test

```bash
asterisk -rx "dialplan reload"
```

Place a test call and check the Asterisk CLI for AMD output:
```bash
asterisk -rvvv | grep AMD
```

## Channel Variables Set by EAGI

| Variable | Values | Description |
|---|---|---|
| `AMDSTATUS` | `HUMAN` or `AMD` | Detection result |
| `AMDCAUSE` | `HUMAN`, `MACHINE`, `CONNECTION_ERROR` | Reason code |
| `AMDSTATS` | JSON string | Raw API response with timing data |

## How It Works

1. Asterisk invokes the Python EAGI script via `EAGI(amdy-amd.py)`
2. The script reads audio from file descriptor 3 (EAGI audio stream)
3. It connects to `ws://api.amdy.io:2700` with the API key from `/etc/amdy/api-key`
4. Audio is streamed at 8kHz 16-bit mono in timed chunks (0.7s, 1s, 2s, 3s)
5. The AMDY API returns a classification (HUMAN or specific machine type)
6. The script sets AMDSTATUS, AMDCAUSE, AMDSTATS channel variables
7. The dialplan routes based on AMDSTATUS

## Troubleshooting

### "File not found" error
- Check script exists: `ls -la /var/lib/asterisk/agi-bin/amdy-amd.py`
- Check permissions: `chmod 755` and `chown asterisk:asterisk`
- Check AGI dir in `/etc/asterisk/asterisk.conf` → `astagidir`

### Python import errors
```bash
pip3 install --force-reinstall pyst2 websocket-client
```

### WebSocket connection failed
- Check API key: `cat /etc/amdy/api-key`
- Test connectivity: `python3 -c "from websocket import create_connection; ws = create_connection('ws://api.amdy.io:2700', header={'X-API-Key': 'YOUR_KEY'}); print('OK'); ws.close()"`
- Check firewall allows outbound to port 2700

### AMDSTATUS always HUMAN
- This is the safety default — on any error, the script returns HUMAN to avoid hanging up on real people
- Check Asterisk logs for the actual error: `asterisk -rvvv | grep AMD`

### IP not registered
```bash
curl -X POST https://app.amdy.io/api/v1/ips/register \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ip":"YOUR_SERVER_IP","description":"asterisk-server"}'
```

## Common Dialplan Patterns

### Queue humans, hangup machines
```asterisk
exten => s,n,EAGI(amdy-amd.py)
exten => s,n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?queue:hangup)
exten => s,n(queue),Queue(sales)
exten => s,n(hangup),Hangup()
```

### Send machines to voicemail
```asterisk
exten => s,n,EAGI(amdy-amd.py)
exten => s,n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?agent:vm)
exten => s,n(agent),Queue(sales)
exten => s,n(vm),VoiceMail(u123@default)
```

### Log all detections to CDR
```asterisk
exten => s,n,EAGI(amdy-amd.py)
exten => s,n,Set(CDR(userfield)=AMD:${AMDSTATUS}:${AMDCAUSE})
exten => s,n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?connect:skip)
exten => s,n(connect),Queue(sales)
exten => s,n(skip),Hangup()
```

## Key Facts

- **Detection speed**: Starts in 1/8 of a second, typical result in 1-3 seconds
- **Accuracy**: 99% across 2.3 billion+ calls/month
- **Audio format**: 8kHz 16-bit mono PCM (standard telephony)
- **Safety**: Defaults to HUMAN on errors (never hangs up on a real person by mistake)
- **Supported platforms**: Any Asterisk 13+ with EAGI support

## Resources

- API docs: https://app.amdy.io/docs/api
- Asterisk install guide: https://app.amdy.io/docs/asterisk-install
- Support: support@amdy.io

## What This Skill Does NOT Cover

- ViciDial/Vicibox installation (use `installamd-v2.sh` instead)
- FreeSWITCH integration (different module, not EAGI)
- SIP configuration or carrier setup
- Dialplan design beyond the AMD integration point
