feat: add initial Hold Slayer AI telephony gateway implementation

Complete project scaffolding and core implementation of an AI-powered
telephony system that calls companies, navigates IVR menus, waits on
hold, and transfers to the user when a human answers.

Key components:
- FastAPI server with REST API, WebSocket, and MCP (SSE) interfaces
- SIP/VoIP call management via PJSUA2 with RTP audio streaming
- LLM-powered IVR navigation using OpenAI/Anthropic with tool calling
- Hold detection service combining audio analysis and silence detection
- Real-time STT (Whisper/Deepgram) and TTS (OpenAI/Piper) pipelines
- Call recording with per-channel and mixed audio capture
- Event bus (asyncio pub/sub) for real-time client updates
- Web dashboard with live call monitoring
- SQLite persistence via SQLAlchemy with call history and analytics
- Notification support (email, SMS, webhook, desktop)
- Docker Compose deployment with Opal VoIP and Opal Media containers
- Comprehensive test suite with unit, integration, and E2E tests
- Simplified .gitignore and full project documentation in README
This commit is contained in:
2026-03-21 19:23:26 +00:00
parent c9ff60702b
commit ecf37658ce
56 changed files with 11601 additions and 164 deletions

104
docs/dial-plan.md Normal file
View File

@@ -0,0 +1,104 @@
# Hold Slayer Gateway — Dial Plan
## Overview
The gateway accepts calls from registered SIP endpoints and routes them
based on the dialled digits. No trunk-access prefix (no "9") is needed.
All routing is pattern-matched in order; the first match wins.
---
## ⚠️ Emergency Services — 911
> **911 and 9911 are always routed directly to the PSTN trunk.**
> No gateway logic intercepts, records, or delays these calls.
> `9911` is accepted in addition to `911` to catch the common
> mis-dial habit of dialling `9` for an outside line.
>
> **Your SIP trunk provider must support emergency calling on your DID.**
> Verify this with your provider before putting this system in service.
> VoIP emergency calling has location limitations — ensure your
> registered location is correct with your provider.
---
## Extension Ranges
| Range | Purpose |
|-------|--------------------------------|
| 2XX | SIP endpoints (phones/softphones) |
| 5XX | System services |
---
## 2XX — Endpoint Extensions
Extensions are auto-assigned from **221** upward when a SIP device
registers (`SIP REGISTER`) with the gateway or via `POST /api/devices`.
| Extension | Format | Example |
|-----------|---------------------------------|--------------------------------|
| 221299 | Auto-assigned to registered devices | `sip:221@gateway.helu.ca` |
### Assignment policy
- First device to register gets **221**, next **222**, and so on.
- Extensions are persisted in the database and survive restarts.
- If a device is removed its extension is freed and may be reassigned.
- `GATEWAY_SIP_DOMAIN` in `.env` sets the domain part of the URI.
---
## 5XX — System Services
| Extension | Service | Notes |
|-----------|----------------------|-----------------------------------------|
| 500 | Auto-attendant | Reserved — not yet implemented |
| 510 | Gateway status | Plays a status announcement |
| 511 | Echo test | Returns audio back to caller |
| 520 | Hold Slayer launch | Prompts for a number to hold-slay |
| 599 | Operator fallback | Transfers to preferred device |
---
## Outbound PSTN
All outbound patterns are routed via the configured SIP trunk
(`SIP_TRUNK_HOST`). No access code prefix is needed.
### Pattern table
| Pattern | Example input | Normalised to | Notes |
|----------------------|--------------------|---------------------|------------------------------------|
| `+1NPANXXXXXX` | `+16135550100` | `+16135550100` | E.164 — pass through as-is |
| `1NPANXXXXXX` | `16135550100` | `+16135550100` | NANP with country code |
| `NPANXXXXXX` | `6135550100` | `+16135550100` | 10-digit NANP — prepend `+1` |
| `011CC…` | `01144201234567` | `+44201234567` | International — strip `011` |
| `00CC…` | `004420…` | `+4420…` | International alt prefix |
| `+CC…` | `+44201234567` | `+44201234567` | E.164 international — pass through |
### Rules
1. E.164 (`+` prefix) is always passed to the trunk unchanged.
2. NANP 11-digit (`1` + 10 digits) is normalised to E.164 by prepending `+`.
3. NANP 10-digit is normalised to E.164 by prepending `+1`.
4. International via `011` or `00` strips the IDD prefix and prepends `+`.
5. 7-digit local dialling is **not supported** — always dial the area code.
---
## Inbound PSTN
Calls arriving from the trunk on the DID (`SIP_TRUNK_DID`) are routed
to the highest-priority online device. If no device is online the call
is queued or dropped (configurable via `MAX_HOLD_TIME`).
---
## Future
- Named regions / area-code routing
- Least-cost routing across multiple trunks
- Time-of-day routing (business hours vs. after-hours)
- Ring groups across multiple 2XX extensions
- Voicemail (extension 500)