Files
hold-slayer/docs/configuration.md
Robert Helewka e2051f7486
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 44s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m53s
docs: document SIP_ENGINE and correct config tables against the models
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>
2026-07-30 17:57:31 -04:00

8.3 KiB

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:

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

# 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

# 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

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