Skip to main content

Starting the Application

Development Mode

Use the provided startup script that handles both the server and ngrok tunnel:
./scripts/dev_start.sh
This script:
  1. Loads environment variables from .env
  2. Starts uvicorn server on configured port (default: 8000)
  3. Starts ngrok tunnel with dual HTTP/WebSocket endpoints
  4. Prints the public URL after tunnel establishment
  5. Shows FastAPI logs in the terminal
Script location: scripts/dev_start.sh

Manual Startup

If you need more control:
# Load environment
export $(grep -vE '^\s*#' .env | xargs)

# Start server
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}

# In separate terminal: start ngrok
ngrok http ${PORT:-8000} --config ops/ngrok.yml

Production Mode

Production deployment currently uses the same in-memory storage as development. For production use, implement persistent storage (PostgreSQL + S3) before deploying.
# Set production environment
export APP_ENV=prod

# Run with production-grade server
gunicorn app.main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --access-logfile - \
  --error-logfile -

Stopping the Application

Graceful Shutdown

# Press Ctrl+C in the terminal running dev_start.sh
# The script handles cleanup of both uvicorn and ngrok processes
The dev_start.sh script includes a trap handler that automatically kills ngrok when the script exits:
cleanup() {
  [[ -n "${NGROK_PID-}" ]] && kill "${NGROK_PID}" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

Force Kill

If the graceful shutdown fails:
# Kill uvicorn processes
pkill -f "uvicorn app.main:app"

# Kill ngrok processes
pkill -f ngrok

Health Checks

Basic Health Check

curl http://localhost:8000/health
Expected response:
{"status": "ok"}

WebSocket Connection Test

Verify WebSocket endpoint is accessible:
wscat -c ws://localhost:8000/ws

External Webhook Test

Test from outside your network (requires ngrok):
curl https://your-ngrok-url.ngrok.io/health

Configuration Updates

Updating Webhook URLs

When your ngrok tunnel URL changes (happens on every restart without a paid plan):
  1. Note the new URL from dev_start.sh output:
    ngrok public URL: https://abc123.ngrok.io
    
  2. Update .env file:
    WS_PUBLIC_URL=wss://abc123.ngrok.io/ws
    HTTP_PUBLIC_URL=https://abc123.ngrok.io
    
  3. Update Telnyx portal:
    • Log in to https://portal.telnyx.com
    • Navigate to “Messaging” or “Voice” webhooks
    • Update webhook URL to: https://abc123.ngrok.io/api/v1/call/incoming
  4. Restart application:
    ./scripts/dev_start.sh
    
Webhook delivery fails silently if URLs are not updated. Always verify URLs match after tunnel restart.

Updating API Keys

# Edit .env
vim .env

# Update the relevant key
OPENAI_API_KEY=sk-new-key-here

# Restart required
./scripts/dev_start.sh

Monitoring Active Calls

Live Call Dashboard

Browser-based debug view showing active calls with real-time updates:
open http://localhost:8000/debug/live_calls/
Displays:
  • Call status (RINGING, ACTIVE, ENDED)
  • Live transcript (streaming)
  • Audio distress metrics
  • Emotion classification
  • Risk level assessment
  • Service category and tags
Update frequency: 1 second

Queue Dashboard

View the dispatcher queue with priority ranking:
open http://localhost:8000/debug/list_queue/
Displays:
  • Queue status (OPEN, IN_PROGRESS, DISPATCHED, RESOLVED)
  • Risk level and score
  • Emergency category (EMS, FIRE, POLICE, OTHER)
  • Call summary
  • Timestamp

Recent Calls

Historical view of processed calls:
open http://localhost:8000/debug/list_calls/

API Operations

Query the Queue

curl http://localhost:8000/api/v1/queue | jq
Response format:
[
  {
    "id": "call_session_123",
    "risk_level": "CRITICAL",
    "risk_score": 0.87,
    "category": "EMS",
    "tags": ["CARDIAC_ARREST", "UNCONSCIOUS"],
    "emotion": "HIGHLY_DISTRESSED",
    "summary": "Caller reports person collapsed and not breathing.",
    "status": "OPEN",
    "created_at": "2026-03-03T23:45:12Z",
    "from_masked": "•••5678",
    "to": "+15551234567"
  }
]

Get Call Details

curl http://localhost:8000/api/v1/calls/{call_id} | jq

Update Queue Status

Mark a call as in progress:
curl -X PATCH http://localhost:8000/api/v1/queue/{call_id}/status \
  -H "Content-Type: application/json" \
  -d '{
    "status": "IN_PROGRESS",
    "note": "Dispatching EMS unit 42"
  }'
Valid status values: OPEN, IN_PROGRESS, DISPATCHED, RESOLVED, CANCELLED

Live Queue (Active Calls)

curl http://localhost:8000/api/v1/live_queue | jq
Returns calls currently streaming (not yet ended).

Data Management

Call Recordings

Recordings are saved locally during development:
# View recordings
ls -lh data/recordings/

# Location referenced in LIVE_SIGNALS dict
# wav_path: data/recordings/{call_id}.wav

Replay a Call

Re-process a saved recording through the pipeline:
python scripts/replay_call.py data/recordings/call_session_123.wav
Use cases:
  • Testing agent improvements
  • Debugging classification issues
  • Training data collection

Export Call Data

Export processed packets for analysis:
python scripts/export_packets.py --output calls.json --limit 100

In-Memory Data Limits

Development mode stores data in memory with fixed limits:
  • Recent calls: 200 (configurable via InMemoryCallStore max_recent parameter)
  • Queue items: unlimited (stored in dict)
Data is lost on application restart. For production, implement persistent storage.
Current storage backend selection (app/main.py:150-154):
if APP_ENV == AppEnv.DEV:
    CALL_STORE: CallStore = InMemoryCallStore(max_recent=200)
else:
    # For now, still in-memory; later drop in Postgres-backed class here.
    CALL_STORE: CallStore = InMemoryCallStore(max_recent=200)

Troubleshooting

Webhooks Not Received

Symptoms:
  • No logs when calling the Telnyx number
  • Incoming calls not appearing in queue
Diagnosis:
# 1. Check server is running
curl http://localhost:8000/health

# 2. Check ngrok tunnel
curl https://your-ngrok-url.ngrok.io/health

# 3. Verify webhook configuration
cat ops/urls.txt

# 4. Check Telnyx portal webhook settings
Solutions:
  1. Verify ngrok is running: ps aux | grep ngrok
  2. Check WS_PUBLIC_URL in .env matches current ngrok URL
  3. Update webhook URL in Telnyx portal
  4. Restart application: ./scripts/dev_start.sh

Audio Stream Not Starting

Symptoms:
  • Call answered but no transcription
  • Empty transcript_live in debug dashboard
Diagnosis:
# Check logs for streaming_start errors
grep "streaming_start" logs.txt

# Verify WS_PUBLIC_URL is set
echo $WS_PUBLIC_URL
Solutions:
  1. Ensure WS_PUBLIC_URL is WebSocket protocol: wss:// not https://
  2. Check ngrok WebSocket tunnel: cat ops/ngrok.yml
  3. Verify Telnyx API key has call control permissions

Transcription Failures

Symptoms:
  • transcript: null in call packets
  • No final transcript after call ends
Diagnosis:
# Check for Deepgram errors
grep "deepgram error" logs.txt

# Test API key
curl -X POST https://api.deepgram.com/v1/listen \
  -H "Authorization: Token $DEEPGRAM_API_KEY" \
  -H "Content-Type: audio/wav" \
  --data-binary @test.wav
Solutions:
  1. Verify DEEPGRAM_API_KEY is set and valid
  2. Check Deepgram account balance/quota
  3. Ensure WAV file was saved: ls data/recordings/
  4. Check file format: 8kHz, 16-bit PCM

High Distress False Positives

Symptoms:
  • Non-emergency calls marked CRITICAL
  • Distress scores too high
Diagnosis:
# Review audio analysis agent
cat app/agents/audio_track.py

# Check distress calculation
grep "distress_score" logs.txt | tail -20
Solutions:
  1. Review distress bucketing logic in compute_risk_level() (app/main.py:225-321)
  2. Adjust thresholds based on observed patterns
  3. Enable OpenAI emotion analysis for better accuracy: EMOTION_PROVIDER=openai
  4. Review semantic tag detection (CRITICAL_TAGS, ELEVATED_TAGS)

Summary Generation Failures

Symptoms:
  • summary: null or generic placeholder text
  • “No transcript available” for valid calls
Diagnosis:
# Check if OpenAI key is set
echo $OPENAI_API_KEY

# Look for summary errors
grep "summary" logs.txt | grep -i error
Solutions:
  1. Set OPENAI_API_KEY for GPT-powered summaries
  2. Without OpenAI key, system falls back to heuristic (first sentence extraction)
  3. Verify transcript exists before summary generation
  4. Check OpenAI account quota/balance

Memory Growth

Symptoms:
  • Application memory usage increasing over time
  • Slower response times
Diagnosis:
# Monitor process memory
ps aux | grep uvicorn

# Check in-memory storage size
curl http://localhost:8000/api/v1/calls | jq length
Solutions:
  1. In-memory store has fixed limits (200 recent calls)
  2. Old calls are automatically rotated out (deque with maxlen)
  3. For production, implement persistent storage to prevent memory issues
  4. Restart application if memory usage is concerning

ngrok Tunnel Disconnects

Symptoms:
  • Webhooks work then suddenly stop
  • ngrok process terminated
Diagnosis:
# Check ngrok process
ps aux | grep ngrok

# Check ngrok logs
tail -f ${NGROK_LOG_FILE:-/tmp/ngrok.log}
Solutions:
  1. Free ngrok tunnels disconnect after 2 hours - restart dev_start.sh
  2. Upgrade to paid ngrok plan for persistent URLs
  3. Implement automatic webhook URL update on tunnel restart
  4. Monitor ngrok API: curl http://127.0.0.1:4040/api/tunnels

Log Analysis

Log Format

Logs are written to stdout in structured format:
# app/core/logging.py
import logging

def setup_logging():
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(message)s"
    )

Key Log Patterns

# Call lifecycle
grep "\[telephony\]" logs.txt

# Audio streaming
grep "streaming_start" logs.txt

# Transcription
grep "\[stt\]" logs.txt

# Emotion analysis
grep "\[emotion\]" logs.txt

# Triage decisions
grep "\[triage:minimal\]" logs.txt

# Summary generation
grep "\[summary\]" logs.txt

Redirecting Logs

# Save logs to file
./scripts/dev_start.sh 2>&1 | tee logs.txt

# Filter errors only
./scripts/dev_start.sh 2>&1 | grep -i error

# JSON formatted logs (future)
# Configure structured logging in app/core/logging.py

Emergency Procedures

System Unresponsive

  1. Check if process is running:
    ps aux | grep uvicorn
    
  2. Check port availability:
    lsof -i :8000
    
  3. Force restart:
    pkill -f "uvicorn app.main:app"
    ./scripts/dev_start.sh
    

Data Loss Prevention

In development mode, ALL DATA IS LOST on restart. For production:
  1. Implement PostgreSQL backend for queue persistence
  2. Implement S3 storage for call recordings
  3. Enable database backups

Critical Call Missed

If a high-priority call was not properly triaged:
  1. Find the call recording:
    ls -lt data/recordings/ | head
    
  2. Replay through pipeline:
    python scripts/replay_call.py data/recordings/call_session_XYZ.wav
    
  3. Manually review classification:
    curl http://localhost:8000/api/v1/calls/call_session_XYZ | jq .risk
    
  4. Report issue for agent tuning

Maintenance

Dependencies

Update Python packages:
pip install -U pip
pip install -r requirements.txt --upgrade

ngrok Configuration

The ops/ngrok.yml file configures dual tunnels:
version: "2"
tunnels:
  api:
    addr: 8000
    proto: http
    inspect: false
  ws:
    addr: 8000
    proto: http
    inspect: false
Both tunnels point to the same port. Telnyx requires HTTP for webhooks and WebSocket upgrade for audio streaming.

Clearing In-Memory State

Simply restart the application:
# All queues and calls cleared
./scripts/dev_start.sh

Performance Tuning

Worker Processes

For production, adjust based on CPU cores:
# General rule: (2 x cores) + 1
gunicorn app.main:app \
  --workers $((2 * $(nproc) + 1)) \
  --worker-class uvicorn.workers.UvicornWorker

Request Timeouts

HTTP client timeouts are hardcoded in app/main.py:
# Telnyx API calls
async with httpx.AsyncClient(timeout=8) as c:

# Deepgram transcription
async with httpx.AsyncClient(timeout=30) as c:

# OpenAI/Deepgram emotion
async with httpx.AsyncClient(timeout=10) as c:
Adjust based on observed latency.

Database Connection Pool

When PostgreSQL is implemented, configure connection pooling:
# Future: app/core/database.py
engine = create_async_engine(
    DATABASE_URL,
    pool_size=20,
    max_overflow=10,
    pool_timeout=30,
)

Build docs developers (and LLMs) love