Started as the SIP_ENGINE row flagged in the last commit. Cross-checking the tables against config.py mechanically (rather than by eye) turned up more, including two entries that were actively wrong. Corrections: - GATEWAY_RTP_PORT_MIN/MAX and GATEWAY_HOST are documented in configuration.md but do not exist — no code reads them and they are absent from .env.example. Setting them today does nothing. Replaced with the real GATEWAY_SIP_ fields (host/port/domain). - GATEWAY_SIP_PORT was documented as 5080 in two places; the code and .env.example both say 5060. - DATABASE_URL was documented with a SQLite default. There is none, and startup exits if it is unset. Additions — every env var the models accept is now documented somewhere (verified bidirectionally: nothing in the models undocumented, nothing documented that the models reject): - Server section: HOST, PORT, DEBUG, LOG_LEVEL, LOG_FORMAT - Safety section: MAX_CONCURRENT_CALLS, USE_MOCK_SIP, SIP_ENGINE - Receptionist section (configuration.md had none, though seven vars exist) Structural staleness, from the PR #8 media-plane work: - core/pjsua_engine.py was absent from the component list and file tree; so were dial_plan.py (the emergency guard) and sip_engine.py. - architecture.md's banner still read "media plane in transition". The engine landed; it is now two selectable engines with the audio consequence stated. - Tech Stack described "single-process async architecture" — the simplification CLAUDE.md explicitly calls out. Now points at the threading model, since there are three execution contexts. - The Asterisk lab shipped in PR #8 with its own README but nothing linked to it. Linked from the test section and both doc indexes. - CLAUDE.md's "no structured JSON logging" gap is closed; test count was 146 across 16 files, now 189 across 19. The other listed gaps (no /metrics, no rate limiting, no health-probe log filter) were re-verified and still hold. Deliberately not hardcoding a test count in the README — that is the same staleness this commit is clearing up. All internal links and anchors verified to resolve; 189 tests pass; lint unchanged at its 216 baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
219 lines
8.3 KiB
Markdown
219 lines
8.3 KiB
Markdown
# Configuration
|
|
|
|
All configuration is via environment variables, loaded through Pydantic Settings. Copy `.env.example` to `.env` and edit.
|
|
|
|
## Environment Variables
|
|
|
|
### Auth (Casdoor SSO + owner)
|
|
|
|
The gateway is **owner-only**: the browser signs in via Casdoor (JWT), MCP/CLI
|
|
clients use owner-minted PATs, and only `OWNER_NAME` may use any surface. With
|
|
`CASDOOR_ENABLED=false` the gateway runs in dev-owner mode — permitted **only** on
|
|
a loopback `HOST`. Startup refuses SSO-enabled-with-missing-config and
|
|
SSO-disabled-off-loopback.
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `CASDOOR_ENABLED` | Enable Casdoor SSO | `false` | No |
|
|
| `CASDOOR_ENDPOINT` | Casdoor base URL | `https://id.ouranos.helu.ca` | If SSO on |
|
|
| `CASDOOR_CLIENT_ID` | Casdoor application client ID | — | If SSO on |
|
|
| `CASDOOR_CLIENT_SECRET` | Casdoor application client secret | — | If SSO on |
|
|
| `CASDOOR_ORG_NAME` | Casdoor organization | `heluca` | No |
|
|
| `CASDOOR_APP_NAME` | Casdoor application name | — | No |
|
|
| `OWNER_NAME` | Casdoor username of the single operator | — | If SSO on |
|
|
| `PUBLIC_BASE_URL` | Public base URL for OAuth discovery (else derived) | — | No |
|
|
|
|
### SIP Trunk
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `SIP_TRUNK_HOST` | Your SIP provider hostname | — | Yes |
|
|
| `SIP_TRUNK_PORT` | SIP signaling port | `5060` | No |
|
|
| `SIP_TRUNK_USERNAME` | SIP auth username | — | Yes |
|
|
| `SIP_TRUNK_PASSWORD` | SIP auth password | — | Yes |
|
|
| `SIP_TRUNK_DID` | Your phone number (E.164) | — | Yes |
|
|
| `SIP_TRUNK_TRANSPORT` | Transport protocol (`udp`, `tcp`, `tls`) | `udp` | No |
|
|
|
|
### Server
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `HOST` | Bind address. Off-loopback requires `CASDOOR_ENABLED=true` | `0.0.0.0` | No |
|
|
| `PORT` | Bind port | `8000` | No |
|
|
| `DEBUG` | SQLAlchemy echo + uvicorn reload | `false` | No |
|
|
| `LOG_LEVEL` | Root log level (`debug`/`info`/`warning`/`error`) | `info` | No |
|
|
| `LOG_FORMAT` | `text` (human-readable) or `json` (structured, for Loki) | `text` | No |
|
|
|
|
`LOG_FORMAT=json` renders one JSON object per line, including uvicorn's access
|
|
log — `method`, `path`, `status_code` (numeric, so it can be range-filtered) and
|
|
`client_addr` arrive as queryable fields rather than a formatted string. The
|
|
Docker image sets it; `text` is the default so local development stays readable.
|
|
|
|
### Safety
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `MAX_CONCURRENT_CALLS` | Cap on simultaneous outbound calls | `4` | No |
|
|
| `USE_MOCK_SIP` | Run the mock SIP engine — no real calls. Must be asked for explicitly; an unconfigured trunk without it fails startup | `false` | No |
|
|
| `SIP_ENGINE` | `sippy` (signalling only — no audio reaches the classifier) or `pjsua2` (call control + media) | `sippy` | No |
|
|
|
|
### Gateway
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `GATEWAY_SIP_HOST` | Bind address for the device-registration listener | `0.0.0.0` | No |
|
|
| `GATEWAY_SIP_PORT` | Port for device SIP registration | `5060` | No |
|
|
| `GATEWAY_SIP_DOMAIN` | SIP domain devices register against | `gateway.local` | No |
|
|
|
|
### LLM
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `LLM_BASE_URL` | OpenAI-compatible API endpoint | `http://localhost:11434/v1` | No |
|
|
| `LLM_MODEL` | Model name for IVR analysis | `llama3` | No |
|
|
| `LLM_API_KEY` | API key (if required) | `not-needed` | No |
|
|
| `LLM_TIMEOUT` | Request timeout in seconds | `30.0` | No |
|
|
| `LLM_MAX_TOKENS` | Max tokens per response | `1024` | No |
|
|
| `LLM_TEMPERATURE` | Sampling temperature | `0.3` | No |
|
|
|
|
### Speech-to-Text
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `SPEACHES_URL` | Speaches/Whisper STT endpoint | `http://localhost:22070` | No |
|
|
| `SPEACHES_MODEL` | Whisper model name | `whisper-large-v3` | No |
|
|
|
|
### Database
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `DATABASE_URL` | PostgreSQL connection string. Startup exits with a readable error if unset | — | Yes |
|
|
|
|
### Notifications
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `NOTIFY_SMS_NUMBER` | Phone number for SMS alerts (E.164) | — | No |
|
|
|
|
### Receptionist
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `RECEPTIONIST_ENABLED` | Answer inbound calls with the AI receptionist | `true` | No |
|
|
| `RECEPTIONIST_GREETING_TEMPLATE` | Spoken greeting | `"Hi, you've reached Robert's line. Who's calling, and what's this about?"` | No |
|
|
| `RECEPTIONIST_MESSAGE_PROMPT` | Spoken prompt before recording a message | `"Please leave your message after the tone."` | No |
|
|
| `RECEPTIONIST_LLM_PERSONA` | System prompt shaping the receptionist's decisions | See `config.py` | No |
|
|
| `RECEPTIONIST_LISTEN_TIMEOUT_S` | Seconds to wait for the caller to speak | `15.0` | No |
|
|
| `RECEPTIONIST_END_OF_UTTERANCE_SILENCE_S` | Silence marking the end of a turn | `1.2` | No |
|
|
| `RECEPTIONIST_MESSAGE_MAX_SECONDS` | Voicemail cap | `90` | No |
|
|
|
|
### Audio Classifier
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `CLASSIFIER_WINDOW_SECONDS` | Audio window size for classification | `3.0` | No |
|
|
| `CLASSIFIER_SILENCE_THRESHOLD` | RMS below this = silence | `0.85` | No |
|
|
| `CLASSIFIER_MUSIC_THRESHOLD` | Spectral flatness below this = music | `0.7` | No |
|
|
| `CLASSIFIER_SPEECH_THRESHOLD` | Spectral flatness above this = speech | `0.6` | No |
|
|
|
|
### Hold Slayer
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `MAX_HOLD_TIME` | Maximum seconds to wait on hold | `7200` | No |
|
|
| `HOLD_CHECK_INTERVAL` | Seconds between audio checks | `2.0` | No |
|
|
| `DEFAULT_TRANSFER_DEVICE` | Device to transfer to | `sip_phone` | No |
|
|
|
|
### Recording
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `RECORDING_DIR` | Directory for WAV recordings | `recordings` | No |
|
|
| `RECORDING_MAX_SECONDS` | Maximum recording duration | `7200` | No |
|
|
| `RECORDING_SAMPLE_RATE` | Audio sample rate | `16000` | No |
|
|
|
|
## Settings Architecture
|
|
|
|
Configuration is managed by Pydantic Settings in `config.py`:
|
|
|
|
```python
|
|
from config import get_settings
|
|
|
|
settings = get_settings()
|
|
settings.sip_trunk_host # "sip.provider.com"
|
|
settings.llm.base_url # "http://localhost:11434/v1"
|
|
settings.llm.model # "llama3"
|
|
settings.speaches_url # "http://localhost:22070"
|
|
settings.database_url # "sqlite+aiosqlite:///./hold_slayer.db"
|
|
```
|
|
|
|
LLM settings are nested under `settings.llm` as a `LLMSettings` sub-model.
|
|
|
|
## Deployment
|
|
|
|
### Development
|
|
|
|
```bash
|
|
# 1. Clone and install
|
|
git clone <repo-url>
|
|
cd hold-slayer
|
|
python -m venv .venv
|
|
source .venv/bin/activate
|
|
pip install -e ".[dev]"
|
|
|
|
# 2. Configure
|
|
cp .env.example .env
|
|
# Edit .env
|
|
|
|
# 3. Start Ollama (for LLM)
|
|
ollama serve
|
|
ollama pull llama3
|
|
|
|
# 4. Start Speaches (for STT)
|
|
docker run -p 22070:8000 ghcr.io/speaches-ai/speaches
|
|
|
|
# 5. Run
|
|
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
|
```
|
|
|
|
### Production
|
|
|
|
```bash
|
|
# PostgreSQL is required (no SQLite fallback)
|
|
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/hold_slayer
|
|
|
|
# Use vLLM for faster inference
|
|
LLM_BASE_URL=http://localhost:8000/v1
|
|
LLM_MODEL=meta-llama/Llama-3-8B-Instruct
|
|
|
|
# Run with multiple workers (note: each worker is independent)
|
|
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1
|
|
```
|
|
|
|
Note: Hold Slayer is designed as a single-process application. Multiple workers would each have their own SIP engine and call state. For high availability, run behind a load balancer with sticky sessions.
|
|
|
|
### Docker
|
|
|
|
```dockerfile
|
|
FROM python:3.13-slim
|
|
|
|
# Install system dependencies for PJSUA2 and Sippy
|
|
RUN apt-get update && apt-get install -y \
|
|
build-essential \
|
|
libpjproject-dev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /app
|
|
COPY . .
|
|
RUN pip install -e .
|
|
|
|
EXPOSE 8000 5080/udp 10000-20000/udp
|
|
|
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
```
|
|
|
|
Port mapping:
|
|
- `8000` — HTTP API + WebSocket + MCP
|
|
- `5080/udp` — SIP device registration
|
|
- `10000-20000/udp` — RTP media ports
|