- Pipeline-down fallback: dialplan plays gogo-fallback after a failed AudioSocket (§15) + script to synthesize the Bosnian prompt via TTS - Voice heartbeat file → /health/voice endpoint + live status in admin panel - Retention job also purges stale call registrations; chat sessions metered; 100%-usage super-admin alert email (GOGO_ADMIN_ALERT_EMAIL) - .env.example, docs/DEPLOY.md (compose stack, GPU node, backups pg_dump, update procedure, security notes) - §16 Definition of Done as an automated test: softphone-style call → fake partner availability → request pushed → webhook confirm → console SMS - M0 PoC scripts: STT accuracy harness (CER/WER + spoken-digit check; dry-run verified with espeak-ng samples + faster-whisper small on CPU — phone number extracted exactly), TTS bake-off (Azure vs ElevenLabs, latency+cost), and end-to-end latency smoke test speaking real AudioSocket to the live pipeline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.5 KiB
3.5 KiB
Deploy — founder's DC (Docker Compose, §3/§16 M6)
Layout
| Service | What | Where |
|---|---|---|
db |
PostgreSQL 16 | any node |
app |
FastAPI backend (dashboard, admin, widget, webhooks, action links) | any node |
voice |
AudioSocket server + Whisper STT | GPU node |
asterisk |
SIP server (softphones, GSM gateway) | host networking, reachable by the gateway |
First install
git clone <repo> && cd gogotelefon
cp .env.example .env # fill everything in; strong GOGO_SECRET_KEY!
docker compose build
docker compose up -d db app # app runs alembic upgrade head on start
docker compose --profile voice up -d voice
# Asterisk config comes from the DB:
docker compose exec app python -m gogo.telephony.asterisk --out ./asterisk/conf \
--backend http://app:8000 --voice voice:9092
python scripts/generate_fallback_audio.py ./asterisk/sounds # needs a TTS key
docker compose --profile voice up -d asterisk
# seed the first admin (or python -m gogo.seed for a full demo tenant):
docker compose exec app python - <<'EOF'
import asyncio
from gogo.auth import hash_password
from gogo.db import get_sessionmaker
from gogo.models import Admin
async def main():
async with get_sessionmaker()() as s:
s.add(Admin(email="you@gogotelefon.ba", password_hash=hash_password("CHANGE-ME")))
await s.commit()
asyncio.run(main())
EOF
Put a TLS reverse proxy (caddy/nginx) in front of app:8000 for
GOGO_BASE_URL — action links and the widget are public.
GPU node for Whisper
Dockerfile.voice installs CPU wheels by default. On the GPU node use the
NVIDIA runtime and set GOGO_WHISPER_DEVICE=cuda, GOGO_WHISPER_MODEL=large-v3
(one RTX 4090/L4 handles 10–15 concurrent calls, §3). Scale by adding voice
containers and pointing different tenants' Asterisk GOGO_VOICE at them.
Operational jobs (built-in, APScheduler in app)
- proposal expiry + owner reminders — every 5 min
- partner polling fallback — every 5 min
- retention: delete call audio older than
GOGO_AUDIO_RETENTION_DAYS(transcripts kept, §5.4), purge stale call registrations — daily 04:00
Monitoring
GET /health(app),GET /health/db,GET /health/voice(heartbeat age)- admin panel → Sistem: DB, LLM key, email/SMS mode, voice heartbeat,
delivery errors from
sms_log/email_log - per-call latency breakdown:
calls.trace.turns[].{stt_ms,llm_ms}
Backups
# nightly dump (cron on the DB node) — keep 14 days
docker compose exec -T db pg_dump -U gogo gogo | gzip > /backup/gogo-$(date +%F).sql.gz
find /backup -name 'gogo-*.sql.gz' -mtime +14 -delete
Recordings volume: rsync /data/recordings if call audio must survive node
loss (transcripts are in the DB either way; audio expires after 90 days).
Restore: gunzip -c dump.sql.gz | docker compose exec -T db psql -U gogo gogo.
Update procedure
git pull
docker compose build app voice
docker compose up -d app voice # alembic migrates on app start
# if workers/tenants changed the SIP layout:
docker compose exec app python -m gogo.telephony.asterisk --out ./asterisk/conf ...
docker compose exec asterisk asterisk -rx 'core reload'
Security notes
- OAuth tokens + partner API keys are encrypted at rest (Fernet from
GOGO_SECRET_KEY) — changing the key orphans them (reconnect/re-enter). /internal/*must not be exposed publicly (compose keeps it on the internal network; the reverse proxy should only forward what's needed).- Action links are signed + single-use; widget is rate-limited per IP.